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/cloudlinux/venv/lib/python3.11/site-packages/wmt
Viewing File: /opt/cloudlinux/venv/lib/python3.11/site-packages/wmt/main.py
#!/opt/cloudlinux/venv/bin/python3 -bb import asyncio import os import sys import aiohttp import signal import time import concurrent.futures from datetime import datetime, timedelta from typing import List, Optional from dataclasses import dataclass from sqlalchemy import or_ from wmt.db import ( ScrapeResult, DomainAlerts, setup_database, session_scope, cleanup_old_data ) from wmt.common.utils import get_domains, setup_logger, save_pid_and_lock, intersect from clsentry import init_sentry_client from clsentry.utils import get_pkg_version from wmt.common.const import ( PING_TIMEOUT_STATUS_CODE, SENTRY_DNS, ERROR_DOMAINS_PING_RETRY_INTERVAL, ERROR_DOMAINS_ALERT_INTERVAL, WMT_LOCK_FILE, PING_CONNECTIONS, LICENSE_EXPIRED_FAREWELL_LETTER_MARKER, LICENSE_CHECK_PAUSE ) from wmt.common.notification import Notifier, SupportedNotificationTypes from wmt.common.report import ErrorReport from wmt.common import cfg from cllicense import CloudlinuxLicenseLib logger = setup_logger('wmt_scanner') def reload_conf(sig_number, frame): cfg.reload() logger.info('Reloading config: %s', str(cfg.to_dict())) def shutdown(sig_number, frame): """ Shutdown to call finally block to close all fds, remove lock and file see: save_pid_and_lock() """ sys.exit(0) @dataclass class ScrapeCoroResult: url: str response_code: Optional[int] = None response_time_ms: Optional[int] = None def get_connection_limit() -> int: value = getattr(cfg.cfg, 'ping_connections', PING_CONNECTIONS) try: value = int(value) except (TypeError, ValueError): value = PING_CONNECTIONS return max(1, value) async def ping(url, ping_timeout, session, semaphore): """ Main 'pinger' 1. Requests domains - if domain responded - keep status code - if no response for timeout - keep Timeout status code - if unreachable (ConnectionError or so) - keep 523 status code (same logic as go implementation) """ async with semaphore: async def _fetch(target_url): start = time.time() try: async with session.get(target_url, timeout=ping_timeout) as resp: # Use the final URL after redirects to preserve actual scheme (http/https) final_url = str(resp.url) if hasattr(resp, 'url') else target_url return ScrapeCoroResult( final_url, response_code=resp.status, response_time_ms=int(1000 * (time.time() - start)) ) except concurrent.futures.TimeoutError: return ScrapeCoroResult(target_url, response_code=PING_TIMEOUT_STATUS_CODE, response_time_ms=ping_timeout * 1000) except aiohttp.client_exceptions.ClientError: # 523 is code for unreachable resource # same logic as in go implementation return ScrapeCoroResult(target_url, response_code=523) # Try HTTPS first, then fallback to HTTP if url.startswith('https://'): https_url = url http_url = 'http://' + url[len('https://'):] elif url.startswith('http://'): https_url = 'https://' + url[len('http://'):] http_url = url else: https_url = f'https://{url}' http_url = f'http://{url}' https_result = await _fetch(https_url) if https_result.response_code == 200: return https_result http_result = await _fetch(http_url) return http_result def executors(ping_timeout, session, semaphore, ping_target_domains=None): """ ping_timeout: specified in config timeout time (s) for request semaphore: semaphore obj to handle asyncio tasks ping_target_domains: mostly needed for re-pinging error domains """ domains = get_domains() if ping_target_domains is not None: domains &= set(ping_target_domains) logger.debug('Those domains will be pinged: %s', str(domains)) for domain in domains: if not cfg.is_domain_ignored(domain): yield ping(domain, ping_timeout, session, semaphore) async def scrape_sites(ping_site_timeout, ping_interval, session, semaphore, ping_target_domains=None) -> List[ScrapeCoroResult]: coroutines = list(executors(ping_site_timeout, session, semaphore, ping_target_domains)) if len(coroutines) == 0: return [] tasks = [asyncio.create_task(coro) for coro in coroutines] try: return await asyncio.wait_for(asyncio.gather(*tasks), timeout=ping_interval) except asyncio.TimeoutError: results: List[ScrapeCoroResult] = [] for task in tasks: if not task.done(): task.cancel() for task in tasks: try: results.append(await task) except asyncio.CancelledError: continue return results def manage_ping_results(engine, pinged, ping_target_domains=None): """ - obtains all scrape coro results from asyncio tasks - saves ping results to ScrapeResult table - updates 'is_resolved' field in DomainAlerts table, in case error domain`s status code was changed to 200 - returns domains with non-200 status code code """ finished_domains = set() errors_domains = {} resolved = [] with session_scope(engine) as session: for result in pinged: session.add(ScrapeResult( website=result.url, is_finished=True, response_code=result.response_code, response_time_ms=result.response_time_ms )) finished_domains.add(result.url) if result.response_code != 200: errors_domains[result.url] = result.response_code else: resolved.append(result.url) # mark no-more failing domains as resolved session.query(DomainAlerts) \ .filter(DomainAlerts.website.in_(resolved)) \ .update(dict(is_resolved=True), synchronize_session=False) domains = get_domains() if ping_target_domains is not None: domains &= set(ping_target_domains) unfinished_domains = set(domains) - finished_domains with session_scope(engine) as session: for unfinished in unfinished_domains: session.add(ScrapeResult( website=unfinished, is_finished=False )) return errors_domains def get_recent_alerts(engine, alert_domains): """ - gets websites that must NOT be included in alert email: less than ERROR_DOMAINS_ALERT_INTERVAL passed or is_resolved marker was not changed from last alerting """ repeat_interval = datetime.now() - timedelta(hours=ERROR_DOMAINS_ALERT_INTERVAL) with session_scope(engine) as session: recently_alerted = session.query(DomainAlerts.website) \ .filter(DomainAlerts.website.in_(list(alert_domains.keys())), or_(DomainAlerts.alert_time > repeat_interval, DomainAlerts.is_resolved == False)) return [row.website for row in recently_alerted] def alert(domains_data): """ prepares needed error report object with error domains to be alerted and sends this mail returns alerted domains """ logger.info('Alerts will be sent for %s', str(list(domains_data.keys()))) error_report = [ ErrorReport( url=domain, code=', '.join(map(str, set(codes))), count_errors=len(codes) ) for domain, codes in domains_data.items() ] Notifier( target_email=cfg.target_email, from_email=cfg.from_email, report={ 'error_report': error_report }, notification_type=SupportedNotificationTypes.ALERT).notify() return domains_data def flush_alerts(engine, alert_domains): """ - gets recently alerted domains (those that must not be alerted again) and does not include them for alerting - calls alerting for left domains - updates DomainAlerts table: if website was not alerted -> adds new record if website was alerted before -> updates alert time and is_resolved marker """ recently_alerted = get_recent_alerts(engine, alert_domains) domains_to_alert = {k: v for k, v in alert_domains.items() if k not in recently_alerted} if not domains_to_alert: logger.info('All domains "%s" were alerted or still not resolved in last %d hours', str(list(alert_domains.keys())), ERROR_DOMAINS_ALERT_INTERVAL) return alert(domains_to_alert) now = datetime.now() with session_scope(engine) as session: websites = session.query(DomainAlerts) \ .with_entities(DomainAlerts.website) \ .all() urls = [row.website for row in websites] for domain in domains_to_alert: if domain in urls: session.query(DomainAlerts) \ .filter(DomainAlerts.website == domain) \ .update(dict(alert_time=now, is_resolved=False)) else: session.add(DomainAlerts(website=domain, alert_time=now)) def should_be_repinged(error_domains): if error_domains and cfg.cfg.alert_notifications_enabled: return True return False def cleanup_farewell_letter_marker(): if os.path.exists(LICENSE_EXPIRED_FAREWELL_LETTER_MARKER): logger.info('CloudLinux license was updated') os.remove(LICENSE_EXPIRED_FAREWELL_LETTER_MARKER) def manage_license_farewell(): """ Sends farewell letter once (if it was not sent before) """ try: if not os.path.exists(LICENSE_EXPIRED_FAREWELL_LETTER_MARKER): logger.warning('Going to send last email about expired license!') Notifier( target_email=cfg.target_email, from_email=cfg.from_email, report={}, notification_type=SupportedNotificationTypes.FAREWELL).notify() except Exception: logger.exception('Error while managing farewell letter') async def scrape_iteration(previously_errored, engine): """ Scanner logic: 1. Scrapes domains and obtains ping results; 2. Manage ping results (e.g: saving to DB) 3. In case error domains found -> start re-pinging Re-pinging: - in min(ping_interval, 5 mins) - flush alerts if needed 4. Sleep for ping_interval until next ping iteration """ start = time.time() # ping interval parameter stored in minutes in config ping_interval_seconds = cfg.cfg.ping_interval * 60 try: cleanup_farewell_letter_marker() cleanup_old_data(engine) connections_limit = get_connection_limit() connector = aiohttp.TCPConnector(limit=connections_limit) async with aiohttp.ClientSession(connector=connector) as session: semaphore = asyncio.Semaphore(connections_limit) ping_result = await scrape_sites( cfg.cfg.ping_timeout, ping_interval_seconds, session, semaphore ) error_domains = manage_ping_results(engine, ping_result) # re-ping if should_be_repinged(error_domains): logger.info('Those domains are unsuccessful: %s \n Try to re-ping them', str(error_domains)) # let`s re-ping in ERROR_DOMAINS_PING_RETRY_INTERVAL time while True: ping_interval_seconds = cfg.cfg.ping_interval * 60 if ping_interval_seconds > ERROR_DOMAINS_PING_RETRY_INTERVAL: elapsed_for_ping = time.time() - start await asyncio.sleep( min(max(ERROR_DOMAINS_PING_RETRY_INTERVAL - elapsed_for_ping, 0), 10) ) if elapsed_for_ping > ERROR_DOMAINS_PING_RETRY_INTERVAL: # re-ping during current ping iteration ping_retry_result = await scrape_sites( cfg.cfg.ping_timeout, ping_interval_seconds, session, semaphore, ping_target_domains=error_domains.keys() ) retry_errors = manage_ping_results( engine, ping_retry_result, error_domains.keys() ) alert_domains = intersect(error_domains, retry_errors) break else: # error domains will be re-pinged together with other domains alert_domains = intersect(previously_errored, error_domains) previously_errored = error_domains break if alert_domains: logger.info('Domains with unsuccessful status code found: "%s"', str(list(alert_domains.keys()))) flush_alerts(engine, alert_domains) else: # clean up, no error domains during current iteration previously_errored = [] except Exception: logger.exception('Error during ping iteration!') finally: while True: ping_interval_seconds = cfg.cfg.ping_interval * 60 # it can be modified with reload elapsed = time.time() - start sleep_time = min(max(ping_interval_seconds - elapsed, 0), 10) # "10" to check "reload" await asyncio.sleep(sleep_time) if elapsed > ping_interval_seconds: break return previously_errored async def scrape_loop(): """ Main loop for wmt_scanner_solo service each 'while: True' iteration returns errored domains (domains that responded with non-200 status code) """ engine = setup_database() previously_errored = [] license_attempt = 0 while True: if CloudlinuxLicenseLib().get_license_status(): license_attempt = 0 previously_errored = await scrape_iteration(previously_errored, engine) else: license_attempt += 1 if license_attempt == 1: logger.warning('Seems your CloudLinux license is expired!') # let`s do several attempts to be really # sure it is not false-positives from CLN or similar if license_attempt >= 5: manage_license_farewell() license_attempt = 0 ping_interval_seconds = cfg.cfg.ping_interval * 60 sleep_time = min(ping_interval_seconds, LICENSE_CHECK_PAUSE) await asyncio.sleep(sleep_time) if __name__ == '__main__': pid = str(os.getpid()) logger.info("PID: %s", pid) with save_pid_and_lock(WMT_LOCK_FILE, pid): init_sentry_client('web-monitoring-tool', get_pkg_version('cl-web-monitoring-tool'), SENTRY_DNS) signal.signal(signal.SIGUSR1, reload_conf) signal.signal(signal.SIGTERM, shutdown) loop = asyncio.get_event_loop() loop.run_until_complete(scrape_loop())