From a058e73f18d65141c08b83c43f4aa85ee7f2972c Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Thu, 11 Jun 2026 12:33:57 +0700 Subject: [PATCH] Fix disabled jobs display and restore points --- .DS_Store | Bin 0 -> 8196 bytes Dockerfile | 18 + README.md | 2 + __pycache__/config.cpython-313.pyc | Bin 0 -> 1278 bytes __pycache__/config.cpython-314.pyc | Bin 0 -> 1538 bytes __pycache__/database.cpython-313.pyc | Bin 0 -> 10681 bytes __pycache__/database.cpython-314.pyc | Bin 0 -> 12873 bytes __pycache__/main.cpython-313.pyc | Bin 0 -> 20235 bytes __pycache__/main.cpython-314.pyc | Bin 0 -> 25299 bytes __pycache__/veeam_client.cpython-313.pyc | Bin 0 -> 19996 bytes __pycache__/veeam_client.cpython-314.pyc | Bin 0 -> 26110 bytes config.py | 20 + database.py | 133 ++++ docker-compose.yml | 17 + main.py | 519 +++++++++++++++ requirements.txt | 9 + static/index.html | 794 +++++++++++++++++++++++ veeam_client.py | 479 ++++++++++++++ veeam_dashboard.db | Bin 0 -> 16384 bytes 19 files changed, 1991 insertions(+) create mode 100644 .DS_Store create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/config.cpython-314.pyc create mode 100644 __pycache__/database.cpython-313.pyc create mode 100644 __pycache__/database.cpython-314.pyc create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 __pycache__/main.cpython-314.pyc create mode 100644 __pycache__/veeam_client.cpython-313.pyc create mode 100644 __pycache__/veeam_client.cpython-314.pyc create mode 100644 config.py create mode 100644 database.py create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 static/index.html create mode 100644 veeam_client.py create mode 100644 veeam_dashboard.db diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5c71b9bbf8663b550c99074f7b4863f673eec28a GIT binary patch literal 8196 zcmeHMUu+ab7@yy^w7YhJZKY5u$DJG?f!1Ct|D=dVTmDp9L9V6OQatZ&mv&{lTW|NS z6jCiA8ci_%H}Q$6=p#|1@n0b5lZg_eX-FiI7<~|5eBuR-=(jt2g%nzEK5@!WEx9dH#$%R z2m}ZO2m}ZO2m}b+4hYbh?Iv-CbD#GHWq?3{!2c2f_x%u|?8>+=$GP-g9aMN%0FtGo z_7c_U9^lzTd>QxUIG3I&`Q*0;_@?l$7~tmAA9071jQet&OK#4<%^Cce;onf;%}#a^ zZ#YAo%b*Mp2oSg)0r&1+LAUi38)lDF5+b!wbUbod{Sw^N-kmyN44Kw3 zz9smt1vY!y7N4Jaf2y4|GPGXTVg?8dyU@xZC`l9{IPXIsN_SMqvqL?}-affMsb?)S zp&!yx%3PapXs70`oK~Ry_hc<2KV;e|r81i~vUwxzXqyeg9Zb5pb~a}j#xBd&9o;f} zla6-KDLR_cmF%;voYU$#ODRg;)^=;;OD||`2q?2ZPD2*qpB zqC*tD2TOX_WQ(dhR?b7?;U%F^uj^{Fs%k22y0&IvRVWea7M1VGM$yfJP7I-E!HFV9 z`2%4r8ehq)(KG6}SMxBI=5g(0>7I-sww$e}ldy~JXBIocj0BQ~HJ8?gyH(1mX7K?Vn4U8dALA2ziZAgszQI|1iwpP_7x5b|;V)dl-$JEOD=Zb33Co2BAtp2l z_X%5tZNe_0SI7th!XV#+P~_&m$vt@$VU)zo#Pi=h!pR#GFT8Dg`$KZaZBM)(=D1Y5 zb6)lQ1(9Vd8&VDn@^^7t)=?6p+zqJIi!yy*8LWP0R!oRk-byNMp!!K177>Uym&+@{N*R%dH&=!0!-_zh<;_^QNmfdTk&8oi#tPhv^%Ub|K<5j$d*Kr(gQske&I~4ct;eC97 z&v6Q;aRy&_44A-=_!+-Ui|Nj(F^x+z$8;f)GY!iepb;)3%9za@`d;oZon@~*i>7+K ziLDauyT-l$Z<+b~|4m#$;5tAcK;XYd0Htk-wia4N@t~f*Aos2vq5K$SH@V}+x%5n^ vaEG{h?>bI$@()9*NBq9HFUPs09!j_TL%=Qm;T@d+!TIm~AD+6s&i{V^ElQ}X literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..67e27ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.13-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN python -c "from main import app; print('App loaded')" && \ + rm -rf __pycache__ */__pycache__ + +EXPOSE 8080 + +ENV DB_URL=sqlite+aiosqlite:////data/veeam_dashboard.db +ENV PYTHONUNBUFFERED=1 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2329510 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Veeam_dashboard + diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d349c1a5aa77f571500bd0adbf5a793bc87eaa2f GIT binary patch literal 1278 zcmZ8h&u<$=6rRnl*Xv)glR8a86R^@otwHg^Arh*hN^M<4QYl4Q!O7BSy`I=h>|N*0 zIFbzq0}@yYhaBi1!5_c{0e^!dA22KRgoNZ9$dR}*Z){5~Yx&#v&6}C`zV~K!n#m-I ze4c%A|MR~zLjFXucw%#7T9d|0;t)rBPL4Ft3TQ7qUpmr3FOUtgLmd4r;usZeU!Sc3 zqhjok{rLtEtLQtVV(!Zb_?#@y-mfEA^;prI;80PE(obB@943Sx)VbuwC$4CCug^E; zWcs}{UXls{%^|SlXrS-P_A|!ZR-x?jMm2Py7Flk) zO#`>ZdJp({(--VL*AHjbL)*4X_607WwZ(!`ts0rND(ygXav*n%5}xZd7!{&9c^i2a z*m;L>L51I9VMp}h@0ZjsG8!Ql{?$8jlF<%9^wzzG8`Rlei|zTrUK_$%#}j@SL`lv( zV1k~p?i+=k_l!)pWTZC>jb(Oph{*z**u5yb5MFv32B&^~f{pGOB^F%tVjXe1_u-G^ zxEPNts^n7|B`Iym6q-!hX-dyKZW9Y5S(egMAGm1x0SiJoV98zNP?JR~3yZSA3v2R> zL~E3C;R@fQt`Oi?JA!daAur=k<)7S~NU%TU47kl(Ve`yCvz=}bHp6A0APR@&W~ z8(=5+{Qd%d6ry-rUg4%+1-!W`ugF@=zypDum0dR5F0epE)|1E1Gg>}8`JbZ5I+z`p zNAN&ygr}3+68_dTO?yS&eML5I%#?O-@Z}AWuYa~kI)C}FZ~c+VURjrqeoqyyQ-!ZA GmGggSH%ux3 literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-314.pyc b/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3ccf999761b3acfdc341d90a67ad98cd7b2e750 GIT binary patch literal 1538 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 Uwm@-n*RAHiKln#v*;V%c0t`n7$p8QV literal 0 HcmV?d00001 diff --git a/__pycache__/database.cpython-314.pyc b/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f7cb4031d37f593d4b21a1a31903c23bba62b60 GIT binary patch literal 12873 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 t{Ck&9(h literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5034696035dcc187a1fc0d5b5dca820355f7df78 GIT binary patch literal 20235 zcmdsfZFF15b>Mq=58qz|0g~XSAbv;!MS`LvQlca)B1L_W6e(Lz(Aptw1R_8RG6~T0 zK-rR;l$P3tTJP4RKPo!Su4(6-jkL{flw?yYX|u5{_gIpBx871h_ ziRDjthIxf)U<5{B`xqiWS;Er1im2eJ>f?HO!n?4H>r?ltiJGSQK25KdXlYvAr|Z=d zeXoHSXkOE2>@^WnubG%>UfXBswGt~$>-vg%ZNx^?`o7{`JF)jVh=b-0eI>o6q_nq; zl=V7^v$vd-_g0XK-bzy0TScmRt4TG*GxpW=t|RMuUBuP9o~(BWF0!I9RX)?>k(f^>4oDy(Z^1s9DmwY~#+1mlFa)Y8#x zgIqo2>S%6z4h>X7J%zOatbM3Xrkv0~VLOBk3XHbK3mYkXC-g&H-49_?8!JNvcLkFf z*{(IP8=>uHe4ntnLnSoj)YuI*&K79Daj02n7CarCU>n*DqiMn3WZv`88t>TxweStC zLz|$^))jU3LY?0)@TQzP@UAA1ift?E?Spz33f0R{Kz`fy6}7t77*|_ft@ahQ_ODTE zN2Zocjh!oMbwjN}JnG2Q>K1kl9RP~##(JmO3>^=yQFBjDO&A@m%?l6Z)jqUF?Y)`W zpUdI&;h`R3->yd)#@_^TfLf@R^bYkN`;*nQh1u8%^J8vx2|&NH8Vd8FtAlqjqwFZ_ zK5z#S2HdP<_lsx4!^4qqH26$(+#mLj1_^+50e>_Y4UGpO%|Gmqo^q=s?Log7?R>aL zG9NyE{NV%73V?uu@5Eu(S?I+lx zzRNEL`y+wi7*y7Y!DuuT9u*~BSEq2m*WGi>t&_B&a46~voRrL?!Kg137Nh>~a8Q&? zxmn5N4+JuWl67(dyPX4oHw1G5dU%&_*gt$KDCw~wnsjrL@kB7_AKyO~3WlRn(L{tq zeIpU#I};=#j8Jk0pq1gMlJ|{}$hhxRM2t$BbZgv`!Lt$!qP#b`TaOIUEB;ogci6B1~bGSEaP_GIYE7U66-G8P#< z+f71ef-SL^WB)bw{rH90U&g*0`$7DC{P~t~|8PX?jtq+}ojomQJX<`iEtF+Y5DI%H z&PrOD5di|DW}NU7yCIrlQU-IZbNDn2KNMz1TY=K7VxTz(-os%k`Js zFS=jaJg=VjzpjfHZ@RocVQWcQ9W#$yd}i93GFq3R9Y*3oviyaax(~`<$z>r{$O}27 zX1A*bGfeg}xEhZ#SJ(l!imZn;r6i;&`8EJRL}ZtX)WJh4@?|6##C$wBPTW`uKd}^| zDQ3Z5cB$jVj+b`N^ReQFKR7tO?R+0;fTCgeWdgDS!*Up+SC}CNZ5*&~Si&_hL;R3h zP+`Alio=w^6U~rT;L%QiQgxKb9`B`g|iHB1XqT;b1rd#he%=5LDGjrpMN+C8iMkJ65eei zNMf=RqYjLAVMH6+jj25tWt)5mvn$&rT>z8#laa`n-0oh)$>ZuigkgkrMFJ6UbtNzF zm#bVib$EJsru*XQ1ivn|GFMT9oS)f#@$(73CRM$Ds%JVn6S(+nf?uCXY(77qkWrWFd8B>(e6pylZ2nC2TmBJ`1X-b*b zx>2*+A{8m~T3P(>APXc_C{U2InVBVXz%Wd;K>%i#WRO>KOjAw_xb+43Oxa0UOqsnO zUPlVC?*JBHglOV+h=6@MFRT{3MW}-C<}AWIkj0XvN9#aX&drdZ6HbRFU|x5GXYy z3oKiq;V7ZRDMXh(YziZMnP`Iu=t2i=M$J@YR|btZ66c?o7{>Kq2^=IiSe}-#?K)V7 zU>VGmD+Y8A7_z4T^9=h6H=s5FmcZ=b&oHm3!5C$9%z$ABH^P!l(1e>M7WwfAJVb0N zz3%D8LL*>b`oly3;KNvo>)jn#@jfxwo_Bxw=+w~>nG8Jnf5+&?t~wvdLKx#{d{EA^(`DV2lT& zry_w&_EgXx04rt%NsQ1el4W@edJm=^a||;10iYQRAX^4dKATfg0M#hz>=U2`3_5^P zFGm&+QvgMR$c+^NILZnv)N~hHRkYm# z824*eP5*$JJ??nlVu@UD~a6E8qC#e8fjeqr?DXsXC@Nk6O4dpMkz`e*x7V9xEC-IKDF zT&kF@NR^addSdp8R9nZJzBhd{flHCuNV2pgUfOcIwDm@90eGUcb*aQ?(kwDYwPw-5 z^s)VH79bmjnHdY6qB9zv%Pxped{MYZ+3c*&?ltNew<=9?mTDN-*s*7FX`uIs{*fRU zO@w>_ntKXLCjwJ_2zXp!gtI4Q5;G|arE*=BV9A$wQMIaxwN^i4IfYxVoN~*9dd0CK&0un74 zy}VcNReOzIlh^FEc&&oTqxKegwO*ZI_L#%AK{U>3DBKId;>|dH^ZDyyyf%0*?nCp! zTKsh}^|0P#od#%a zKu(RYBg{kGI%b5Wt#^Kg*7^IiK3uT%fAtw!SKp`gvjtlp&ublYN!XAx;%8Ua8n_7- z)wlEN`6)&JIpj+vSO+&NeDNBTS=7P^MVWsfTP+O!6Koz+xY%1Ps+2xIfSiE*%9f&z zL0b_Pc*@@_SJ$KF`!s3GYekU=v^3qt17s@<;hg4l|Q8=zA0=$D*V3nJu`zUjD!UQSp0UU< z7{+@&8LY2DgCfp&w8>*7;Wx_XI=>OR#{N+KJ@vIye_>7T?7y)!;TcGJj>kR66P^>X z&yUAF6?LZzus zMiDOrQ7)~?7(&M(`gd58A)@2V8~VA{Z#;JOsW&ITHF@pP@BDsp$B`SgiRS)fvk-3< z63yP&W0Udb$xDySKJteC9tlJB-zCza5%sA;t460JBc_$K7h!MuVqxj07P|CNP@B=xK=D=7Jk3Br0{^MZ)NJmGtP?^^wR} zP}0yz0xAlXv1G#7mv(eWRuUYB9XRZyWYsL0sA3L9VW$p9K^p&nE zw`t8z5cH7|+=!4bV)P}9@W7KyVe}kC?o!$Lh}MVfDZtYN*@cKMhphKd(!#M>7}TO< z7zsv)Px4~}~BgM5ZzTeX~!Ie0P3kNA(U#Nh%0=PoaF zTPl6@**?{sGFV<{y4Wab`qjXxIa#l>6Qzd27hE%C@+6b=rY0VXS7s@tT%IO>5HGI^DCV3)Q}Q|lU%>vqQ1?M$rOl`P*i-HT9L%C#Zs+7@?hOSsyS z6>ZafcXdpOb58%#z_fm$xFlW@e!V6C{r?m?t)b<^SU#L6 z9)@zn4`4o!C=N_(f8p6ZXPzIPCvj)vTgB7nMGXUvgN?HrQ{YDEneE9wQf1YbzBK!# zRCzW2FRe;Bsy{BTO;y&WDqI*>)-D+})s{sI<7!S-ZAg`_U)ty>HcV^oHZfM`9RHg3 zRqcG`<&lJ?EvajZ>Dp4(lEn>-wd94di(}7+le((7uIg8Cz*V&2S4B+u)_+>CI+qz1 zNC*dS{TKV^4$imE*UdF0O&fn?+W6Z=4vK#BD;-n)kb>EyVD`>GH9s7yXiS@|p!;lbHw^@Q&Xu*5(ol;Qvz$f#rCs9Ij!mSFndG`RkQDrd{?PGjn|#+hgFbZ|5=H zVei?)ylY~6I{0_ZJf>}39njo+2DaD1zh_iqy2uW%e6Nn}E!DhNug0{SMR*enuX(S< zaM;Pc*Woy9=Wdua0KZ{Z1LTI2!*nGJFTPP@=+SaFHaU7!+>Jemb5o@T$W1Nc+%&Mz zm78WmPdj(B%+b@z-P}xZTGa??r#KxfbnfOJL$8gyrNPo$CN-vQJf=%o=+!N!;Rwgw zs(q+$FZcdB7Siv#c<>0lUyuENe+S#QOY{Ct4W{?1q4WcegVGN)jw9>24=UIr)w&NV zHJDz{0nRX}m`p%}o;sKUzyjHz&zkXTZC%6Q2C_3JSW(piCLEamE{2KN3VCytjXt%1 zO2CnnG4H(@JK$JeuXE5XWE#!Kt#RGcqer{pg+5Am;m;;vr z@8zI}+hK#E&T&Jj2Q%K_Hh2#PrLTU-_oI(jtw3n+>j1(ZLx)^m$hulwOh25M;YhqZ zZte&EL{zzeLSfj9L#~HzP`ClfCf|VS@5@R9+f*abW&H%+Yo2c-+yIv~*1!N|~)w2Y+IAOtTBdk{SP3%ag{s zxUueUO^%-$P1CK4=34(_Q`M55v6L(t7*p|7|56!a+i*`Z^)G1{>wfm0xC8oi-qN)N z4l}wo^0PWfz3S+4^WU;U^fpru`M2xYE)V~9!$wHIQ{=?ht$|oFD^#P)`-+`G=A7aL zNPq6JP1eB++gc7@PJ_ULOA91lnHS2YHJs!26jW>Aa5?R8YvAxXIQ$wo>Kq*P8aSF9 z9CY~=>a#Wn2VH)JaCA91=<+LsqX&nZ>d|6)X@&zAo7$^Emzq~E=<#I2Z5UWVavms2 zw-?7DAuAkg7IZ195MV=o$a)P{Oh=fY0-N>#xY8Z~l{~>@`&QoQvjhs6D=Kmt9ugZ` zwd0i5`%X@V#sZ4HU0_rHGpH(}=az2ji}rn`f99!gdarn|?oKv;?vicR_Lcs7q7KT> zmv3TU({Eb2aYv0u2aJ1_oB?lGd%#;5MJE*`2xvQ{JWT+g40VBUlmNVJ(c@)Fx)m58 z{{fRn7s5aEz3N2ip@jVq;G?_X$oIw*B}Wp) zN2dC~Jy5kdRaKj+Zi-j8rAlGvzbRE#mvT1H|MjUFcits|bH*0`M5qOByo^Op4a0CU z0kzcy_$?PhBro#vrlg)xjD8Jzcqc_k6?sy=R|i{#oGX)(MsAke1n_U*C*lvmv$PJP%hJLTu$R*}VsWo4au9*)k#&w$ zzBQSV8WMw2Eeg$(kf4)j4Oi-dtvY44z3|w@$FdK+XH-2~tvujjl4;hIf@?~rFP>hq z>WVeLW^^XaRQKIt#MBmLUKX|lGz7rUk+X|qvwMdiTgV$j`B|G* zT6bOs8vdrZCw=|W1((BJBj6DaxH*DSLvTe`rgI$AlsBji71o&HK5O>t6!JGvREmtj zEfJl~w(7Yj6Sl^rv2m*VBcpY}QaQIjw!ST4X^-jJ7mVO+NE2Agm^PR@v}6v?9#C-BN1YXzN{?d%rtICU5FHhH-Q;=fTT>_%ARIGJ3JlheZPCW$?NUI3iH6A;5(NUQln57rO0q zu4o3boXFl+_K_Qa4!3IwYEGFacx_DLCL$BC?ogf}%8C^bPW;9rXR;O~`3NxF$c00Y zpp(X0Gxu!5x;d%eJk|A)(VRDFc*B=_pX;3|pW8jx9J6k^Tz{KyNtudf>ZkX__%d*` z8Jf~Gx571pS+-Ndzt{$;bL;P?rdr@jkLU`v=%71vy5SW27hnrcXBx7W#wt~p`>oe5 zaM7v?vryRwDHGIR5B?*54eZR_xi!i)N^64F!-0cZuk=&U3Hq!9+Yi%kjTQ`oQ4uD% z7;J+3XDY$cp`vPF7F@;SRH&>uLPecg$H}wsD!P=y6&4g6@`3`m^FM$I2kEiyIkPwX zWH|D4*hSAH&ly2+D8}Ixm0a&Bs0Zo4h6g>ujlvl-e9@NG>T}qz3nLsJDEy3mr)Qlj z7hp5WMpkX$zI;0h>`Rb%04;B?pFf_kHzv)EQwKlNnrAj#YMgDH8%!3vT!{q7r@|7N&phzUc9!;ib|B{M7_;83=}D&m5Eows&v1mW+h<6mrm)SGNq1Ancv36k$L zfR~v)SC%&$aE#5w|8(IwFapX!o&*2f#v!zqQBctDbol@EhOVG`bbq1o3Y>?7J8!5+ z^8#9l81i+4e8X~JGt4J#-d(^BdK`onC=IC22T?K8kUh{5G%zo<9jf%*J$0x00eS$u zT6K31YUbVD`?PW7wX)*wo=(2Icbt3-{Utwx=pgw!O#U1qxP6jmUgHj^(A5osx|{qP zjQ$WKoageT3Qdl+{cjQa#~4vngDdc=(-fzZ}?EQK; z$u7q97CXSNbL@Vj<~py&w4Md{bt8*uv!T0)yIxk(ZRD;uv6yb=yY0;NR;=|d&vt7y z@2WMJHmafY-69T3-?clsJ>0t++3rT&yPGta_Hclc_77w(1xaT3RAV>@S5rn{B{;Q0 zg`skx%lJP)LQ!GLLK$$t{sFishwMd7IH}F-G4RCH3)WC}pRv}Igp;!DqMq49hB>dw z%YE88uhQl*qtct~Qz;Wv9xluenw1iP^=9n&%&Ba5dUqq)Gvx94M@LBz4qAh-&{4m{ z05ldYcYFG({Q9fl>A3?iNzB3L|;esKQ?zpM|){x{l60Kg&+8_jIl z&L-y}OCvANkP*n?4Ecr8xnOg~%02M)^|a^ zo;vh#No~qru~1rbDe__@QMxf%+8QryO_Xk1(()F~)WN$3MqLssX}o;!Hoq-pw85FG zjVW>8qh6)+?#`|3pRt`=_-}NoA^9nG->Q$kaV%6|YN7M8z+^?_KHJh9TP<(9wPIVE z6|4Yg=&ZmfhPYyVf!Jg=TvP`2K6*X3J>?T~)Q%t9=&EyIAA2^sh0K>+0oTvqF3|3C!%Eg#Sl@F_OL3fq)to=Y|~ ze9jOG2ZGOdPDRJZVEXIe9dsEas77R8Z3Cw2Ad=J+mn=d0i%4DgPD7@yvFxkdR}arW z6R&Ey+7z$an{a+EVcZwv_obb->(;{`6oD`D;ZHUAeCKrd+yz%7;e{S-9M^2Tx;_^1 z2hyK`9&`_oKSeNqT8xCrdQ2M9f23qAbdqpbk4LC>0-Y*qH~(rttdUr${SXzUPJNElLrZJ5sB9~$w=7c~jJPfDGE)KEb)j696d zZp2kbCnw;Bta=0pBY)0I9>PL8P3R2SjM;q{9l)p$BLN~w2jr!H3xPa>SquJ|lAIA+z;ErskJS!>?5wtNJwq(K1HA)-miOY@B%*3>Nq1 zDa|wwpV~Lfspq0|#&~gk%v>MWH6)nEC5@Ubnz1i3@R-|1AD8Q|I<9Q~?+95eV%ZII zb&KGQ-!OmtEw5((1vAf`Fb=-C6*lJ9}<8G+|;AihL@VHySv5q`TRgKLUs$|oI>EBl^T#iDzdm>uZmhA@i$rB& zi|4xM9k2ExdhudSDO9bWkG|fx2(C%ZRcHksi|vd`{pGUf%BHImY}t~EWvj5W&P7Zu zn$m?aefj13Sj*?)jr*p`Vr=IUjBN+vb+I_i9dMrsT5&$-j2S9on|I%^PdQ_3|Dwvs z!WrW-0}n(V0AxrmR%Ke<7~6Rm8|+zB@qj8UGw?vvUO34z+!!-gXIafVo7CTi|4oBW#EBGm5T^~E3?Wdni6cY zj06LKqHDl~G%j=_7kAozp$gg2$znqm(y5uee>~eJ$_?*f`52X&F~fUE8G7CuXA{< literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-314.pyc b/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5571a7f3ad921364854a8ff193cf72c8ad70c8e7 GIT binary patch literal 25299 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 BTCo5C literal 0 HcmV?d00001 diff --git a/__pycache__/veeam_client.cpython-313.pyc b/__pycache__/veeam_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8811c86d3095a6bc0de4c7cf5a6ae83130de6600 GIT binary patch literal 19996 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 literal 0 HcmV?d00001 diff --git a/__pycache__/veeam_client.cpython-314.pyc b/__pycache__/veeam_client.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4dbe540ec3549f03d87b8876ab0e13c30858aa8a GIT binary patch literal 26110 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 list[Instance]: + async with async_session() as session: + result = await session.execute(select(Instance).order_by(Instance.name)) + return list(result.scalars().all()) + + +async def get_instance(instance_id: int) -> Instance | None: + async with async_session() as session: + result = await session.execute(select(Instance).where(Instance.id == instance_id)) + return result.scalar_one_or_none() + + +async def add_instance(name: str, host: str, port: int, username: str, password: str, + version: str = "auto", notes: str = "") -> Instance: + async with async_session() as session: + inst = Instance( + name=name, host=host, port=port, username=username, + password=password, version=version, notes=notes + ) + session.add(inst) + await session.commit() + await session.refresh(inst) + return inst + + +async def update_instance(instance_id: int, **kwargs) -> Instance | None: + async with async_session() as session: + result = await session.execute(select(Instance).where(Instance.id == instance_id)) + inst = result.scalar_one_or_none() + if not inst: + return None + for key, value in kwargs.items(): + if hasattr(inst, key): + setattr(inst, key, value) + inst.updated_at = datetime.utcnow() + await session.commit() + await session.refresh(inst) + return inst + + +async def delete_instance(instance_id: int) -> bool: + async with async_session() as session: + result = await session.execute(select(Instance).where(Instance.id == instance_id)) + inst = result.scalar_one_or_none() + if not inst: + return False + await session.delete(inst) + await session.execute(sa_delete(CacheEntry).where(CacheEntry.instance_id == instance_id)) + await session.commit() + return True + + +async def set_cache(instance_id: int, key: str, data: dict): + async with async_session() as session: + result = await session.execute( + select(CacheEntry).where( + CacheEntry.instance_id == instance_id, + CacheEntry.key == key + ) + ) + entry = result.scalar_one_or_none() + if entry: + entry.data = json.dumps(data) + entry.fetched_at = datetime.utcnow() + else: + entry = CacheEntry( + instance_id=instance_id, + key=key, + data=json.dumps(data) + ) + session.add(entry) + await session.commit() + + +async def get_cache(instance_id: int, key: str) -> dict | None: + async with async_session() as session: + result = await session.execute( + select(CacheEntry).where( + CacheEntry.instance_id == instance_id, + CacheEntry.key == key + ) + ) + entry = result.scalar_one_or_none() + if entry: + return {"data": json.loads(entry.data), "fetched_at": entry.fetched_at.isoformat()} + return None diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..57abf52 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.8" + +services: + veeam-dashboard: + build: . + container_name: veeam-dashboard + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - data:/data + environment: + - DB_URL=sqlite+aiosqlite:////data/veeam_dashboard.db + - CACHE_TTL=60 + +volumes: + data: diff --git a/main.py b/main.py new file mode 100644 index 0000000..a07efa7 --- /dev/null +++ b/main.py @@ -0,0 +1,519 @@ +import asyncio +import json +import time +from contextlib import asynccontextmanager +from datetime import datetime +from pathlib import Path + +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +from config import settings, BASE_DIR +from database import ( + init_db, get_instances, get_instance, add_instance, + update_instance, delete_instance, set_cache, get_cache +) +from veeam_client import VeeamClient, port_for_version, detect_version_from_host + + +_cache: dict[str, tuple[float, dict]] = {} + + +def _cached(key: str, ttl: int = None) -> dict | None: + ttl = ttl or settings.cache_ttl + entry = _cache.get(key) + if entry and (time.time() - entry[0]) < ttl: + return entry[1] + return None + + +def _set_cache_inmem(key: str, data: dict): + _cache[key] = (time.time(), data) + + +# ─── Pydantic models ─────────────────────────────────────────────── + +class InstanceCreate(BaseModel): + name: str + host: str + port: int | None = None + username: str + password: str + version: str = "auto" + notes: str = "" + + +class InstanceUpdate(BaseModel): + name: str | None = None + host: str | None = None + port: int | None = None + username: str | None = None + password: str | None = None + version: str | None = None + is_active: bool | None = None + notes: str | None = None + + +class InstanceResponse(BaseModel): + id: int + name: str + host: str + port: int + username: str + version: str + notes: str + is_active: bool + created_at: str + updated_at: str + + +class DashboardResponse(BaseModel): + instances: list + summary: dict + updated_at: str + + +# ─── App lifecycle ───────────────────────────────────────────────── + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_db() + yield + +app = FastAPI(title=settings.app_name, version="1.0", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# ─── Helpers ─────────────────────────────────────────────────────── + +def _serialize_instance(inst) -> dict: + return { + "id": inst.id, + "name": inst.name, + "host": inst.host, + "port": inst.port, + "username": inst.username, + "version": inst.version, + "notes": inst.notes, + "is_active": inst.is_active, + "created_at": inst.created_at.isoformat() if inst.created_at else "", + "updated_at": inst.updated_at.isoformat() if inst.updated_at else "", + } + + +def _safe_str(v) -> str: + if v is None: + return "" + if isinstance(v, dict): + return str(v.get("result") or v.get("Result") or "unknown") + return str(v) + + +async def _collect_instance(inst) -> dict | None: + port = inst.port or port_for_version(inst.version) + ver = inst.version + if ver == "auto": + ver = detect_version_from_host(inst.host) + port = port_for_version(ver) + + client = VeeamClient(inst.host, port, inst.username, inst.password, ver) + try: + role = await client.connect() + jobs_data = [] + sessions_data = [] + repos_data = [] + repo_states = [] + alarms_data = [] + agents_data = [] + errors = [] + + try: + jobs_data = await client.fetch_jobs() + except Exception as e: + errors.append(f"jobs: {e}") + jobs_data = [] + + try: + sessions_data = await client.fetch_sessions(100) + except Exception as e: + errors.append(f"sessions: {e}") + sessions_data = [] + + if not jobs_data and sessions_data: + job_map: dict[str, dict] = {} + for s in sessions_data: + name = s.get("jobName") or s.get("name") or s.get("Name") or "" + if not name: + continue + ts = s.get("endTime") or s.get("creationTime") or "" + if name not in job_map or ts > (job_map[name].get("_ts") or ""): + job_map[name] = { + "name": name, + "type": s.get("jobType") or s.get("type") or "", + "result": s.get("result") or "", + "state": s.get("state") or "", + "last_backup": ts, + "schedule_enabled": True, + "_ts": ts, + } + for j in job_map.values(): + j.pop("_ts", None) + jobs_data = list(job_map.values()) + + try: + repos_data = await client.fetch_repositories() + except Exception as e: + errors.append(f"repos: {e}") + + try: + repo_states = await client.fetch_repository_states() + except Exception as e: + errors.append(f"repo states: {e}") + + try: + alarms_data = await client.fetch_alarms() + except Exception as e: + pass + + try: + agents_data = await client.fetch_agents() + except Exception as e: + errors.append(f"agents: {e}") + + protection_groups = [] + try: + protection_groups = await client.fetch_protection_groups() + except Exception as e: + errors.append(f"protection groups: {e}") + + try: + agent_policies = await client.fetch_agent_policies() + except Exception as e: + errors.append(f"agent policies: {e}") + agent_policies = [] + + backups_data = [] + try: + backups_data = await client.fetch_backups() + except Exception as e: + errors.append(f"backups: {e}") + + restore_points = [] + try: + restore_points = await client.fetch_restore_points(200) + except Exception as e: + errors.append(f"restore points: {e}") + + for s in sessions_data: + if isinstance(s.get("result"), dict): + s["result"] = s["result"].get("result") or s["result"].get("Result") or "unknown" + if "jobName" in s and "job_name" not in s: + s["job_name"] = s["jobName"] + if "creationTime" in s and "creation_time" not in s: + s["creation_time"] = s["creationTime"] + if "endTime" in s and "end_time" not in s: + s["end_time"] = s["endTime"] + + job_last_backup = {} + for s in sessions_data: + s_name = s.get("jobName") or s.get("name") or s.get("Name") or "" + s_end = s.get("endTime") or s.get("end_time") or s.get("creationTime") or "" + if s_name and s_end: + if s_name not in job_last_backup or s_end > job_last_backup[s_name]: + job_last_backup[s_name] = s_end + + for j in jobs_data: + if isinstance(j.get("result"), dict): + j["result"] = j["result"].get("result") or j["result"].get("Result") or "unknown" + if not j.get("result") and j.get("lastResult"): + j["result"] = j["lastResult"] + if not j.get("state") and j.get("status"): + j["state"] = j["status"] + if "scheduleEnabled" in j and "schedule_enabled" not in j: + j["schedule_enabled"] = j["scheduleEnabled"] + if "restorePointsToKeep" in j and "restore_points_to_keep" not in j: + j["restore_points_to_keep"] = j["restorePointsToKeep"] + if "nextRun" in j and "next_run" not in j: + j["next_run"] = j["nextRun"] + j_name = j.get("name") or j.get("Name") or "" + if j_name in job_last_backup: + j["last_backup"] = job_last_backup[j_name] + + rp_per_backup: dict[str, int] = {} + for rp in restore_points: + bid = rp.get("backupId") or "" + if bid: + rp_per_backup[bid] = rp_per_backup.get(bid, 0) + 1 + backup_to_job: dict[str, str] = {} + for b in backups_data: + bid = b.get("id") or "" + jid = b.get("jobId") or "" + if bid and jid: + backup_to_job[bid] = jid + rp_per_job: dict[str, int] = {} + for bid, count in rp_per_backup.items(): + jid = backup_to_job.get(bid, "") + if jid: + rp_per_job[jid] = rp_per_job.get(jid, 0) + count + for j in jobs_data: + jid = j.get("id") or j.get("Id") or "" + if jid in rp_per_job: + j["actual_restore_points"] = rp_per_job[jid] + + job_count = len(jobs_data) + success = sum(1 for j in jobs_data if _safe_str(j.get("result") or "").lower() in ("success", "true")) + failed = sum(1 for j in jobs_data if _safe_str(j.get("result") or "").lower() in ("failed", "error", "false")) + running = sum(1 for j in jobs_data if _safe_str(j.get("state") or "").lower() == "running") + warning = sum(1 for j in jobs_data if "warning" in _safe_str(j.get("result") or "").lower()) + disabled = sum(1 for j in jobs_data if str(j.get("schedule_enabled") or "").lower() in ("false", "no", "0") or _safe_str(j.get("state") or "").lower() == "disabled") + + last_sessions = sessions_data[:20] if sessions_data else [] + + result = { + "id": inst.id, + "name": inst.name, + "host": inst.host, + "port": port, + "version": client.version, + "is_online": True, + "user_role": role, + "errors": errors, + "jobs": jobs_data, + "jobs_summary": { + "total": job_count, + "success": success, + "failed": failed, + "running": running, + "warning": warning, + "disabled": disabled, + }, + "disabled_policies": disabled, + "recent_sessions": last_sessions, + "repositories": repos_data, + "repository_states": repo_states, + "alarms": alarms_data, + "agents": agents_data, + "agent_policies": agent_policies, + "protection_groups": protection_groups, + "restore_points": restore_points[:50] if restore_points else [], + } + + await set_cache(inst.id, "dashboard", result) + return result + + except Exception as e: + error_result = { + "id": inst.id, + "name": inst.name, + "host": inst.host, + "port": port, + "version": ver, + "is_online": False, + "error": str(e), + "jobs": [], + "jobs_summary": {"total": 0, "success": 0, "failed": 0, "running": 0, "warning": 0, "disabled": 0}, + "disabled_policies": 0, + "recent_sessions": [], + "repositories": [], + "repository_states": [], + "alarms": [], + "agents": [], + "agent_policies": [], + "protection_groups": [], + "restore_points": [], + } + return error_result + finally: + await client.close() + + +async def _collect_all() -> list[dict]: + instances = await get_instances() + tasks = [_collect_instance(inst) for inst in instances if inst.is_active] + if not tasks: + return [] + results = await asyncio.gather(*tasks, return_exceptions=True) + output = [] + for r in results: + if isinstance(r, Exception): + continue + if r: + output.append(r) + return output + + +def _build_summary(instance_data: list[dict]) -> dict: + total_instances = len(instance_data) + online = sum(1 for i in instance_data if i.get("is_online")) + total_jobs = sum(i.get("jobs_summary", {}).get("total", 0) for i in instance_data) + total_success = sum(i.get("jobs_summary", {}).get("success", 0) for i in instance_data) + total_failed = sum(i.get("jobs_summary", {}).get("failed", 0) for i in instance_data) + total_running = sum(i.get("jobs_summary", {}).get("running", 0) for i in instance_data) + total_warning = sum(i.get("jobs_summary", {}).get("warning", 0) for i in instance_data) + total_alarms = sum(len(i.get("alarms", [])) for i in instance_data) + total_repos = sum(len(i.get("repositories", [])) for i in instance_data) + + return { + "total_instances": total_instances, + "online_instances": online, + "offline_instances": total_instances - online, + "total_jobs": total_jobs, + "successful_jobs": total_success, + "failed_jobs": total_failed, + "running_jobs": total_running, + "warning_jobs": total_warning, + "total_alarms": total_alarms, + "total_repositories": total_repos, + } + + +# ─── API Endpoints ───────────────────────────────────────────────── + +@app.get("/api/health") +async def health(): + return {"status": "ok"} + + +# Instance CRUD + +@app.get("/api/instances", response_model=list[InstanceResponse]) +async def list_instances(): + instances = await get_instances() + return [_serialize_instance(i) for i in instances] + + +@app.post("/api/instances", response_model=InstanceResponse, status_code=201) +async def create_instance(data: InstanceCreate): + port = data.port or port_for_version(data.version) + inst = await add_instance( + name=data.name, + host=data.host, + port=port, + username=data.username, + password=data.password, + version=data.version, + notes=data.notes, + ) + return _serialize_instance(inst) + + +@app.get("/api/instances/{instance_id}", response_model=InstanceResponse) +async def read_instance(instance_id: int): + inst = await get_instance(instance_id) + if not inst: + raise HTTPException(404, "Instance not found") + return _serialize_instance(inst) + + +@app.put("/api/instances/{instance_id}", response_model=InstanceResponse) +async def edit_instance(instance_id: int, data: InstanceUpdate): + updates = data.model_dump(exclude_unset=True) + if not updates: + raise HTTPException(400, "No fields to update") + inst = await update_instance(instance_id, **updates) + if not inst: + raise HTTPException(404, "Instance not found") + return _serialize_instance(inst) + + +@app.delete("/api/instances/{instance_id}") +async def remove_instance(instance_id: int): + ok = await delete_instance(instance_id) + if not ok: + raise HTTPException(404, "Instance not found") + _cache.pop(f"instance_{instance_id}", None) + return {"ok": True} + + +# Test connection — MUST be before {instance_id} routes to avoid path conflicts + +@app.post("/api/test-connection") +async def test_connection_raw(data: InstanceCreate): + port = data.port or port_for_version(data.version) + ver = data.version + if ver == "auto": + ver = detect_version_from_host(data.host) + port = port_for_version(ver) + client = VeeamClient(data.host, port, data.username, data.password, ver) + try: + role = await client.connect() + return {"ok": True, "role": role, "version": client.version} + except Exception as e: + return JSONResponse(status_code=502, content={"ok": False, "error": str(e)}) + finally: + await client.close() + + +@app.post("/api/instances/{instance_id}/test") +async def test_instance(instance_id: int): + inst = await get_instance(instance_id) + if not inst: + raise HTTPException(404, "Instance not found") + result = await _collect_instance(inst) + if result and result.get("is_online"): + return {"ok": True, "role": result.get("user_role"), "version": result.get("version")} + return JSONResponse( + status_code=502, + content={"ok": False, "error": result.get("error", "Unknown error") if result else "No response"} + ) + + +# Dashboard + +@app.get("/api/dashboard") +async def dashboard(force_refresh: bool = False): + if not force_refresh: + cached = _cached("dashboard_aggregate") + if cached: + return cached + + instance_data = await _collect_all() + summary = _build_summary(instance_data) + result = { + "instances": instance_data, + "summary": summary, + "updated_at": datetime.utcnow().isoformat(), + } + _set_cache_inmem("dashboard_aggregate", result) + return result + + +# Per-instance data + +@app.get("/api/instances/{instance_id}/data") +async def instance_data(instance_id: int, force_refresh: bool = False): + inst = await get_instance(instance_id) + if not inst: + raise HTTPException(404, "Instance not found") + + if not force_refresh: + cached = _cached(f"instance_{instance_id}") + if cached: + return cached + + result = await _collect_instance(inst) + _set_cache_inmem(f"instance_{instance_id}", result) + if not result.get("is_online"): + return JSONResponse(status_code=502, content=result) + return result + + +# Serve frontend + +@app.get("/") +async def index(): + return FileResponse(str(BASE_DIR / "static" / "index.html")) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host=settings.host, port=settings.port, reload=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..66774e7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi>=0.110.0 +uvicorn[standard]>=0.29.0 +httpx>=0.27.0 +sqlalchemy[asyncio]>=2.0.30 +greenlet>=3.0.0 +aiosqlite>=0.20.0 +pydantic>=2.7.0 +pydantic-settings>=2.2.0 +pycryptodome>=3.20.0 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..3a235b4 --- /dev/null +++ b/static/index.html @@ -0,0 +1,794 @@ + + + + + +Veeam Dashboard + + + + +
+
+

Veeam Dashboard

+
+ + +
+
+ +
+ +
+
Инстансы
+
Задания
+
Успешно
+
Ошибки
+
Warning
+
Running
+
+ +

Нет добавленных инстансов

+ +
+ Загрузка... + +
+
+ + + + + + + diff --git a/veeam_client.py b/veeam_client.py new file mode 100644 index 0000000..4e9099c --- /dev/null +++ b/veeam_client.py @@ -0,0 +1,479 @@ +import base64 +import httpx +import re +from datetime import datetime + + +# Legacy API (v9.5-v11) — used in x-api-version header for session auth +API_VERSIONS_LEGACY = { + "9.5": "1.0-rev1", + "10": "1.0-rev1", + "11": "1.0-rev1", +} + +# v12+ OAuth2 API — used in x-api-version header for /api/oauth2/token and data requests +# 1.0-rev2 is for v11 (legacy). For v12+, use 1.1-rev2 as safe default. +API_VERSIONS_V12 = { + "auto": "1.1-rev2", + "12": "1.1-rev0", + "12.1": "1.1-rev1", + "12.2": "1.1-rev2", + "12.3": "1.2-rev1", + "13": "1.3-rev1", +} + + +def detect_version_from_host(host: str) -> str: + h = host.lower() + for v in ["12.3", "12.2", "12.1", "12", "11", "10", "9.5"]: + if v in h: + return v + return "auto" + + +def port_for_version(version: str) -> int: + try: + ver = float(version) + except ValueError: + return 9419 + return 9419 if ver >= 12 else 9398 + + +class VeeamClient: + def __init__(self, host: str, port: int, username: str, password: str, version: str = "auto"): + self.host = host + self.port = port + self.username = username + self.password = password + self.version = version + self.base_url = f"https://{host}:{port}" + self.token = None + self.session_id = None + self._v12_mode = False + self._legacy_api_version = "1.0-rev1" + self._v12_api_version = "1.1-rev2" + self._http = httpx.AsyncClient(verify=False, timeout=30.0) + + async def close(self): + if self.session_id: + try: + await self._http.delete( + f"{self.base_url}/api/session/{self.session_id}", + headers=self._legacy_headers() + ) + except Exception: + pass + await self._http.aclose() + + def _legacy_headers(self) -> dict: + h = { + "Accept": "application/json", + "x-api-version": self._legacy_api_version, + } + if self.session_id: + h["X-RestSvcSessionId"] = self.session_id + return h + + def _v12_headers(self) -> dict: + h = { + "Accept": "application/json", + "Content-Type": "application/json", + "x-api-version": self._v12_api_version, + } + if self.token: + h["Authorization"] = f"Bearer {self.token}" + return h + + async def connect(self) -> str: + return await self._try_connect() + + def _is_v12(self) -> bool: + return self._v12_mode + + def _should_try_v12_first(self) -> bool: + if self.version == "auto": + return True + try: + return float(self.version) >= 12 + except ValueError: + return True + + def _resolve_v12_revision(self) -> str: + return API_VERSIONS_V12.get(self.version, "1.0-rev2") + + def _resolve_legacy_revision(self) -> str: + return API_VERSIONS_LEGACY.get(self.version, "1.0-rev1") + + async def _try_connect(self) -> str: + combos = [] + default_port = self.port or 9419 + if self.version == "auto": + combos = [ + (9419, "v12"), + (9398, "legacy"), + ] + elif self._should_try_v12_first(): + combos = [ + (default_port, "v12"), + ((9398 if default_port == 9419 else 9419), "legacy"), + ] + else: + combos = [ + (default_port, "legacy"), + ((9419 if default_port == 9398 else 9398), "v12"), + ] + + last_error = "" + for port, method in combos: + self.port = port + self.base_url = f"https://{self.host}:{self.port}" + try: + if method == "v12": + self._v12_api_version = self._resolve_v12_revision() + result = await self._connect_v12() + self._v12_mode = True + else: + self._legacy_api_version = self._resolve_legacy_revision() + result = await self._connect_legacy() + self._v12_mode = False + return result + except Exception as e: + last_error = f"{type(e).__name__}: {e}" + continue + + raise ConnectionError( + f"Cannot connect to {self.host} (tried ports {[c[0] for c in combos]}). " + f"Last error: {last_error}" + ) + + async def _connect_legacy(self) -> str: + auth_str = base64.b64encode(f"{self.username}:{self.password}".encode()).decode() + resp = await self._http.post( + f"{self.base_url}/api/session", + headers={ + "Accept": "application/json", + "x-api-version": self._legacy_api_version, + "Authorization": f"Basic {auth_str}", + }, + ) + data = self._parse(resp) + sid = data.get("sessionId") or data.get("SessionId") + if not sid: + msg = data.get("_raw", resp.text[:300]) if isinstance(data, dict) else str(data)[:300] + raise ConnectionError(f"Legacy auth failed ({resp.status_code}): {msg}") + self.session_id = sid + return data.get("userRole") or data.get("UserRole") or "unknown" + + async def _connect_v12(self) -> str: + resp = await self._http.post( + f"{self.base_url}/api/oauth2/token", + data={ + "grant_type": "password", + "username": self.username, + "password": self.password, + }, + headers={ + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + "x-api-version": self._v12_api_version, + }, + ) + if resp.status_code == 404: + raise ConnectionError( + f"v12 auth endpoint not found on {self.base_url} " + f"(404 — REST API may be disabled or wrong port)") + try: + data = resp.json() + except Exception: + raise ConnectionError( + f"v12 auth failed ({resp.status_code}): {resp.text[:200]}") + self.token = data.get("access_token") + if not self.token: + raise ConnectionError( + f"Token auth failed ({resp.status_code}): " + f"{data.get('error_description', resp.text[:200])}") + return data.get("user_role") or data.get("username", "unknown") + + def _resolve_version(self) -> str: + if self.version != "auto": + return self.version + detected = detect_version_from_host(self.host) + if detected != "auto": + return detected + return "12" + + async def fetch_jobs(self) -> list[dict]: + if self._is_v12(): + for path in ("/api/v1/jobs/states", "/api/v1/jobs"): + try: + data = await self._fetch_json_list(path) + if data: + return data + except Exception: + continue + for t in ( + "LinuxAgentBackup", "WindowsAgentBackup", + "LinuxAgentBackupServerPolicy", "WindowsAgentBackupServerPolicy", + "LinuxAgentBackupWorkstationPolicy", "WindowsAgentBackupWorkstationPolicy", + "VSphereBackup", "HyperVBackup", "VSphereReplica", + "CloudDirectorBackup", "EntraIDTenantBackup", "EntraIDAuditLogBackup", + "FileBackupCopy", "LegacyBackupCopy", "BackupCopy", + "ObjectStorageBackup", "EntraIDTenantBackupCopy", "SureBackupContentScan", + "CloudBackupAzure", "CloudBackupAWS", "CloudBackupGoogle", + ): + for base in ("/api/v1/jobs/states", "/api/v1/jobs"): + try: + data = await self._fetch_json_list(f"{base}?typeFilter={t}") + if data: + return data + except Exception: + continue + try: + return await self._fetch_legacy_jobs() + except Exception: + pass + return [] + return await self._fetch_legacy_jobs() + + async def fetch_sessions(self, limit: int = 50) -> list[dict]: + if self._is_v12(): + seen: set[str] = set() + all_sessions: list[dict] = [] + session_types = [ + "BackupJob", "BackupCopyJob", "AgentBackup", "AgentPolicy", + "FileBackupHealthCheck", "ReplicaJob", "MalwareDetection", + "SureBackup", "SureBackupContentScan", + "UnstructuredDataBackupCopy", "ArchiveBackup", "ArchiveCopy", + "CopyBackup", "ConfigurationBackup", "ConfigurationResynchronize", + "Retention", + ] + filters = "&".join(f"typeFilter={t}" for t in session_types) + for path in ( + f"/api/v1/sessions?limit={limit * 3}", + f"/api/v1/sessions?limit={limit}&{filters}", + ): + try: + items = await self._fetch_json_list(path) + for s in items: + sid = s.get("id") or s.get("Id") or "" + if sid and sid not in seen: + seen.add(sid) + all_sessions.append(s) + except Exception: + continue + if all_sessions: + return all_sessions + return all_sessions + return await self._fetch_legacy_sessions(limit) + + async def fetch_repositories(self) -> list[dict]: + if self._is_v12(): + return await self._fetch_json_list("/api/v1/backupInfrastructure/repositories") + return await self._fetch_legacy_repos() + + async def fetch_repository_states(self) -> list[dict]: + if self._is_v12(): + return await self._fetch_json_list("/api/v1/backupInfrastructure/repositories/states") + return [] + + async def fetch_alarms(self) -> list[dict]: + if self._is_v12(): + try: + return await self._fetch_json_list("/api/v1/alarms") + except Exception: + return [] + return [] + + async def fetch_backup_servers(self) -> list[dict]: + if self._is_v12(): + return await self._fetch_json_list("/api/v1/backupInfrastructure/managedServers") + return await self._fetch_legacy_list("/api/backupServers", "BackupServer") + + async def fetch_agents(self) -> list[dict]: + if self._is_v12(): + return await self._fetch_json_list("/api/v1/agents/protectedComputers") + return [] + + async def fetch_protection_groups(self) -> list[dict]: + if self._is_v12(): + try: + return await self._fetch_json_list("/api/v1/agents/protectionGroups") + except Exception: + return [] + return [] + + async def fetch_agent_policies(self) -> list[dict]: + if self._is_v12(): + seen: set[str] = set() + all_policies: list[dict] = [] + policy_types = [ + "WindowsAgentBackupWorkstationPolicy", + "LinuxAgentBackupWorkstationPolicy", + "WindowsAgentBackupServerPolicy", + "LinuxAgentBackupServerPolicy", + ] + for t in policy_types: + for base in ("/api/v1/jobs/states", "/api/v1/jobs"): + try: + items = await self._fetch_json_list(f"{base}?typeFilter={t}") + for p in items: + pid = p.get("id") or p.get("Id") or "" + if pid and pid not in seen: + seen.add(pid) + all_policies.append(p) + except Exception: + continue + return all_policies + return [] + + async def fetch_backups(self) -> list[dict]: + if not self._is_v12(): + return [] + try: + return await self._fetch_json_list("/api/v1/backups") + except Exception: + pass + seen: set[str] = set() + all_backups: list[dict] = [] + backup_types = [ + "LinuxAgentBackup", "WindowsAgentBackup", + "LinuxAgentBackupServerPolicy", "WindowsAgentBackupServerPolicy", + "LinuxAgentBackupWorkstationPolicy", "WindowsAgentBackupWorkstationPolicy", + "VSphereBackup", "HyperVBackup", "VSphereReplica", + "CloudDirectorBackup", "EntraIDTenantBackup", "EntraIDAuditLogBackup", + "FileBackupCopy", "LegacyBackupCopy", "BackupCopy", + "ObjectStorageBackup", "EntraIDTenantBackupCopy", "SureBackupContentScan", + "CloudBackupAzure", "CloudBackupAWS", "CloudBackupGoogle", + ] + for t in backup_types: + try: + items = await self._fetch_json_list(f"/api/v1/backups?jobTypeFilter={t}") + for b in items: + bid = b.get("id") or b.get("Id") or "" + if bid and bid not in seen: + seen.add(bid) + all_backups.append(b) + except Exception: + continue + return all_backups + + async def fetch_restore_points(self, limit: int = 100) -> list[dict]: + if self._is_v12(): + return await self._fetch_json_list(f"/api/v1/restorePoints?limit={limit}") + return [] + + async def _fetch_json_list(self, path: str) -> list[dict]: + resp = await self._http.get( + f"{self.base_url}{path}", + headers=self._v12_headers(), + ) + if resp.status_code >= 400: + raise ConnectionError(f"GET {path}: {resp.status_code} {resp.text[:200]}") + data = resp.json() + return data.get("data") or data.get("items") or data.get("results") or data.get("jobs") or (data if isinstance(data, list) else []) + + async def _fetch_legacy_jobs(self) -> list[dict]: + resp = await self._http.get( + f"{self.base_url}/api/jobs", + headers=self._legacy_headers(), + ) + if resp.status_code >= 400: + raise ConnectionError(f"GET /api/jobs: {resp.status_code} {resp.text[:200]}") + data = self._parse(resp) + jobs = data.get("Jobs") or data.get("jobs") or data.get("Refs", {}).get("Job") or [] + if isinstance(jobs, dict): + jobs = [jobs] + return [self._normalize_legacy_job(j) for j in jobs] + + async def _fetch_legacy_sessions(self, limit: int) -> list[dict]: + resp = await self._http.get( + f"{self.base_url}/api/sessions?limit={limit}", + headers=self._legacy_headers(), + ) + if resp.status_code >= 400: + return [] + data = self._parse(resp) + sessions = data.get("Sessions") or data.get("sessions") or [] + if isinstance(sessions, dict): + sessions = [sessions] + return [self._normalize_legacy_session(s) for s in sessions] + + async def _fetch_legacy_repos(self) -> list[dict]: + return await self._fetch_legacy_list("/api/repositories", "BackupRepository") + + async def _fetch_legacy_list(self, path: str, item_key: str) -> list[dict]: + resp = await self._http.get( + f"{self.base_url}{path}", + headers=self._legacy_headers(), + ) + if resp.status_code >= 400: + return [] + data = self._parse(resp) + items = data.get(f"{item_key}s") or data.get(item_key) or [] + if isinstance(items, dict): + items = [items] + return items + + def _parse(self, resp: httpx.Response) -> dict: + ct = resp.headers.get("content-type", "") + if "json" in ct: + try: + return resp.json() + except Exception: + pass + text = resp.text + if text.strip().startswith("<"): + return self._xml_to_dict_simple(text) + try: + return resp.json() + except Exception: + return {"_raw": text[:500]} + + def _xml_to_dict_simple(self, xml: str) -> dict: + result = {} + tags = re.findall(r"<(\w+)([^>]*)>(.*?)", xml, re.DOTALL) + for tag_name, attrs_str, content in tags: + attrs = dict(re.findall(r'(\w+)="([^"]*)"', attrs_str)) + sub = self._xml_to_dict_simple(content) if "<" in content else content.strip() + if attrs: + entry = {"_text": sub} if sub else {} + entry.update(attrs) + else: + entry = sub + if tag_name not in result: + result[tag_name] = entry + else: + existing = result[tag_name] + if not isinstance(existing, list): + result[tag_name] = [existing] + result[tag_name].append(entry) + return result + + def _normalize_legacy_job(self, job: dict) -> dict: + return { + "id": job.get("@Id") or job.get("id") or job.get("Id", ""), + "name": job.get("@Name") or job.get("name") or job.get("Name", ""), + "type": job.get("@Type") or job.get("type") or job.get("JobType", ""), + "description": job.get("@Description") or job.get("description", ""), + "schedule_enabled": job.get("@ScheduleEnabled") or job.get("scheduleEnabled", ""), + "is_active": job.get("@IsBackupToD2D") or job.get("isActive", ""), + "next_run": job.get("@NextRun") or job.get("nextRun", ""), + "restorePointsToKeep": job.get("@RestorePointsToKeep") or job.get("restorePointsToKeep", ""), + "lastResult": job.get("@LastResult") or job.get("lastResult", ""), + } + + def _normalize_legacy_session(self, s: dict) -> dict: + return { + "id": s.get("@Id") or s.get("id", ""), + "name": s.get("@Name") or s.get("name", ""), + "job_name": s.get("@JobName") or s.get("jobName", ""), + "job_id": s.get("@JobId") or s.get("jobId", ""), + "type": s.get("@Type") or s.get("type", ""), + "result": s.get("@Result") or s.get("result", ""), + "state": s.get("@State") or s.get("state", ""), + "creation_time": s.get("@CreationTime") or s.get("creationTime", ""), + "end_time": s.get("@EndTime") or s.get("endTime", ""), + } + + def __repr__(self): + return f"VeeamClient({self.host}:{self.port} v{self.version})" diff --git a/veeam_dashboard.db b/veeam_dashboard.db new file mode 100644 index 0000000000000000000000000000000000000000..eb9de2d5ae0d01dd98014c7b0195e96affa49c5c GIT binary patch literal 16384 zcmeI%O-sWt7zgmyisE46tw$d`Z6F@hdA4fMY1e73q6ZnVjaYQ;XyV(!gNh%)yC27o zWmep@y6x`x2fB25d79^!UP2EK+*k>^NV7Iqv|`K|rfIAb8HQo$Yf@jm^83Hp>ljg6 z#?sqISr4$vjxpOVzv~JG0uX=z1Rwwb2tWV=5P-n{6gab{?3!&p#z`dZ~!%NYM z6RCI->YFHUnB4T46ELcKTkM1e4Wp(<`B19-m|T6@otU<5+kDpX&aXv2BLA_zA_mTe z%X%|QGjXJ!5wLCMlh+K$J96Dc(#_xN8Q8yYB_0;&*zq@a9KW)*S}n|uxZ)JB6CLkI z%nPA(s!>ay8@F_BLA}8W{ri5s;rOSt$4+%lQMFWknKP&D`FZm-&6DXZO7?Fi3Q6^w zsWLRNgttY1-OIIFap9M#RKt5aX{H8h+(?m)4&32V-lbVIJov51WSl1b0V~VJr6s8n z@-J#ETRc?pt)Pu&(`AlVAPqCY)d=#Nj^3&f{eQQldp6gHeiR5m00Izz00bZa0SG_< z0uX=z1jbkZ=l?O@T%-m82tWV=5P$##AOHafKmY;|_!1~}YkdF5`v3t5KmY;|fB*y_ O009U<00IygYk^Mz=h6@W literal 0 HcmV?d00001