From 9bcb1db7567cfeb11a6a2b5693b925a53c560473 Mon Sep 17 00:00:00 2001 From: David Edell Date: Sat, 29 Nov 2025 16:28:41 -0500 Subject: [PATCH 01/51] Grafana updates including: Upgraded to v12, switched internal DB to PostgresSQL, consolidated dashboard+widget definitions and moved to a text provisioning file, and updated anms-ui monitor to use the new dashboard. Address #282 and #283 --- .env | 2 + .../components/management/monitor/Monitor.vue | 12 +- anms.Containerfile | 10 +- docker-compose.yml | 15 +- grafana/create_grafana_db.sql | 3 + grafana/grafana.ini | 3 - grafana/grafana_vol/environment/.gfenv | 6 - grafana/grafana_vol/grafana.db | Bin 1814528 -> 0 bytes .../provisioning/dashboards/anms-monitor.json | 619 ++++++++++++++++++ .../provisioning/dashboards/dashboards.yaml | 13 + .../provisioning/datasources/datasource.yml | 16 +- 11 files changed, 656 insertions(+), 43 deletions(-) create mode 100644 grafana/create_grafana_db.sql delete mode 100644 grafana/grafana_vol/environment/.gfenv delete mode 100644 grafana/grafana_vol/grafana.db create mode 100644 grafana/provisioning/dashboards/anms-monitor.json create mode 100644 grafana/provisioning/dashboards/dashboards.yaml diff --git a/.env b/.env index 3144792f..23945427 100644 --- a/.env +++ b/.env @@ -42,6 +42,8 @@ DB_HEALTHCHECK_PASSWORD=healthcheck GRAFANA_CONTAINER_PORT=3000 GRAFANA_HOST_PORT=grafana:${GRAFANA_CONTAINER_PORT} GRAFANA_PROXIES_PATH=localhost/grafana +GRAFANA_DB_NAME=grafana_internal_db + REDIS_PORT=6379 JS_AMP_PORT=3001 ANMS_UI_HTTP_PORT=9030 diff --git a/anms-ui/public/app/components/management/monitor/Monitor.vue b/anms-ui/public/app/components/management/monitor/Monitor.vue index 0b472be5..2059e679 100644 --- a/anms-ui/public/app/components/management/monitor/Monitor.vue +++ b/anms-ui/public/app/components/management/monitor/Monitor.vue @@ -1,14 +1,6 @@ diff --git a/anms.Containerfile b/anms.Containerfile index 29b6c214..ef08af1d 100644 --- a/anms.Containerfile +++ b/anms.Containerfile @@ -74,6 +74,10 @@ COPY deps/dtnma-adms /usr/src/dtnma-adms # This is a postgres stateful database with data definition startup SQL scripts FROM docker.io/library/postgres:14 AS anms-sql +# Grafana DB Creation (can't setup second DB via env variable) +COPY grafana/create_grafana_db.sql /docker-entrypoint-initdb.d/ + +# ANMS Table Setup COPY deps/anms_db_tables/*.sql /docker-entrypoint-initdb.d/ COPY deps/dtnma-tools/refdb-sql/postgres/Database_Scripts/*.sql /docker-entrypoint-initdb.d/ @@ -152,7 +156,7 @@ HEALTHCHECK --start-period=10s --interval=60s --timeout=10s --retries=20 \ # Local grafana configuration # -FROM docker.io/grafana/grafana:9.1.3 AS grafana +FROM docker.io/grafana/grafana:12.3.0 AS grafana # Optional APL network configuration from # https://aplprod.servicenowservices.com/sp?id=kb_article&sys_id=c0de6fe91b83d85071b143bae54bcb34 @@ -164,9 +168,7 @@ RUN ( \ ) || true USER grafana -COPY --chown=grafana grafana/grafana_vol /var/lib/grafana -COPY grafana/provisioning /etc/grafana/provisioning -COPY grafana/plugins /var/lib/grafana/plugins +COPY --chown=grafana grafana/provisioning /etc/grafana/provisioning COPY grafana/grafana.ini /etc/grafana/grafana.ini diff --git a/docker-compose.yml b/docker-compose.yml index 28f671ab..394e1db0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -219,11 +219,16 @@ services: depends_on: - grafana-image-renderer environment: - - GF_RENDERING_SERVER_URL=http://${RENDERER_HOST_PORT}/render - - GF_RENDERING_CALLBACK_URL=http://${GRAFANA_HOST_PORT}/ - - GF_SERVER_ROOT_URL=http://${ANMS_GW_FQDN}/grafana/ - volumes: - - "grafana-data:/var/lib/grafana" + GF_RENDERING_SERVER_URL: http://${RENDERER_HOST_PORT}/render + GF_RENDERING_CALLBACK_URL: http://${GRAFANA_HOST_PORT}/ + GF_SERVER_ROOT_URL: http://${ANMS_GW_FQDN}/grafana/ + GF_DATABASE_TYPE: postgres + GF_DATABASE_HOST: postgres:5432 + GF_DATABASE_NAME: ${GRAFANA_DB_NAME} + GF_DATABASE_USER: ${DB_USER} + GF_DATABASE_PASSWORD: ${DB_PASSWORD} + GF_DATABASE_SSL_MODE: disable + GF_DATABASE_PATH: "" # Explicitly disable internal sqlite for clarity grafana-image-renderer: hostname: grafana-image-renderer diff --git a/grafana/create_grafana_db.sql b/grafana/create_grafana_db.sql new file mode 100644 index 00000000..384ecbd8 --- /dev/null +++ b/grafana/create_grafana_db.sql @@ -0,0 +1,3 @@ +-- create_grafana_db.sql +CREATE DATABASE grafana_internal_db; +GRANT ALL PRIVILEGES ON DATABASE grafana_internal_db TO grafana; diff --git a/grafana/grafana.ini b/grafana/grafana.ini index 5d0d411c..91895549 100644 --- a/grafana/grafana.ini +++ b/grafana/grafana.ini @@ -10,9 +10,6 @@ allow_embedding=true [live] allowed_origins=* -[server] -serve_from_sub_path=true - [users] allow_sign_up=false auto_assign_org=true diff --git a/grafana/grafana_vol/environment/.gfenv b/grafana/grafana_vol/environment/.gfenv deleted file mode 100644 index 22447818..00000000 --- a/grafana/grafana_vol/environment/.gfenv +++ /dev/null @@ -1,6 +0,0 @@ -GF_DATABASE_TYPE=postgres -GF_DATABASE_HOST=postgres:5432 -GF_DATABASE_NAME=amp_core -GF_DATABASE_USER=postgres -GF_DATABASE_PASSWORD=password -GF_DATABASE_SSL_MODE=disable diff --git a/grafana/grafana_vol/grafana.db b/grafana/grafana_vol/grafana.db deleted file mode 100644 index 04c31b0b04fe71f81cdcbbcb050ed096c2237e78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1814528 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 (r4{UCHFmdOTTOGl0I#X(*3&IrO(>^ z(r3+W(x-i&<0-~&sj}TF{l4oynw{E?Tj+0V!*S{UOWLI0^-k(;Zcy(hSM9HysrY=wko7UE)3O&l@90nT78pB|6{lAA9S#~7}K^^f!!gB-e_brz}kcsxDs_QFfVhwaSbWhaQ8r7NzRLDN zYHM0*Yo`1|lYt4idDQ7{g@3HOwRtMeuko4Y)@F~(-RX37I6W@oj}{V(62Q~i9028V zVaouuHAeL(%0{$_580WZ*aB?0Sd68HD0F$%GE!=ZLd!G|3u`_lix&o0VxL0iq2+{` zD7T20?97DdE+Yy_M7FCzfNm#`&TAQgWFs1_^CY@?bD6B-gLY<6yn73osKJl8Vh}`U zXV5@EvJ_Zqj|*}|;K-4bmEz)dQpoo6wxgn&*)v)Nlci8x%~^Ddo22zeWfu8Vd?nA9 z)EH7sTF!|j8L&?REn1eW)OL*Kxm?gP<);Od497_6ltI`nZtO;QBi3sYUV%xCv}m%* zsI?nZc*V0h`Mu)0otfOSW$5GEt`g||?u4zWP=!n4nw{}qB~3%>*tBkg_N=rEl|>2p zPY!{>%YNFe2WEm3p$WZFI*G|{47-+>-l$GkavH0>=BW^$`&~#*Ll|8*T?pb4owb&O(883of$Sjz?${jjF`7G z=S7=Q4#gJUGmt`wVtW~u#GH)@CW|B>wj-6~%jDjeZP;RBur_Iy-3P_2of#1KLt5H^ zsl(i6?C*5rfzFfCDN3zR=dk@)a6-(Gj%-m*-_Q%|B2tD*SsKL}ZP{LU`=F+>s$-~} zj@|;T*&x3muGpD?xVJ1lDZ_O&vO+L_~*8;h#MwqqA5tzA|En&;=pg05*hI3p%( z%;G_UmJSBmI4SQaYa{zZNynLlEvxxt4*Fqs)h$1XxlA~EFWbfC8b-8SS%FyTGZw!N zyAfdvo^Db=V>LW!$?3_%(W=}0c}wPafhSwcb(xP++1ra8!## zb|iAab1ZqjYi_1;WtSoXmx<9*sWm_Jzt!=3jN?mi!9OH` z1dsp{Kmter2_OL^fCP{L5n3EDU40Ke%}^h&e$5NB{{S0VIF~kN^@u0!RP}AOR%sq9;&Y zX<^{;zY0F^`Ts><8xQ~zKmter2_OL^fCP{L5)bT$zYlbmj zNB{{S0VIF~kN^@u0!RP}AOR$R1YV>BDk~U^wW5Nlsjv6~vyVB=I9}7xQvYjvzPqQl z?oaCmYd==oQ}g>Z!}gr*zpeM#BGo^tK2-H4=5*!%R?a~H)4$8YrmgYVG0P+Sb6h0B z=d*k!%;j@S;heC{r;2_)*gGEZhXQQKe{v|m&KJ4Pvqx*^sEIPqh5|Fjkm(G+suR}4ha}-5 zpURmyMiX&x2*=YVZd$tGEYGL3Jn4laE@2L$Rx+$)DI?^#93L~f>9QQn@LRg))3Ghx zA>U9sMezeppvkZ2_)LmRfY{k=Tu6ltWWi6%<=gYN#)AhfS1%e%TrJIfnNoh%$~P&* z@(I3NDUb}56unGvp6!GB7zz#t3{^^yY6TaK0tBfhD{)AaWDgmBA;V|ox^C)&wt{eO zBdv;Oh=5;D$1@vxDGbi>m|5a#5#_b4k%9sHARlS$J!TOP$5SzWJ=ixBR?9?+NBSL) z72SSVF0kN8Utorv-y+~VJ33<4Zl$IS;@)i4aB2u1J`NWML*p zf@6~bXc|swxaYv##=Ai8iOF?{bgm;+Ha9~mv$_k7DZXfbY5D-x(!?7z+sEn ztqKKw=_+)m2_d>17Seo%%aH~NrLJ?hSXLgLCu3+y9Gx>X%y~onfM|T()_C}^rLasJ zxz4lh>!zHx+@0%OcG=X8LX@^zph5a1U5Bab6cb!F7v|UbXr5p)@zZi)&DPj?&?45E zv=3U$x|Xpa=gqWD$exSJm0(m{TEtscZH=Rl*aIes(Q=o1Evcu`CA_{>!X^PtgwXk# zZ=I*VNYlKOvo-pgE#g5{qJ*#*CnHdfPo{IUou;@XZ@9ZwDvrTj4JFB8a8rPgNTEti zWo?bk%}SLr_%!@jHC8Kw+b$~>DHbY4X`PCv!(1!|-FQ~%z;$&>9VXNfT6Sdn0{#BU zp%Ck;ZQ)jIjmP&{#Co;i=D5Z1GQSaC*4X+1Ky>%vJG46X!-7BTR!r=YV0DUO$sLdYz_%q5%=61*Xz^mbzC0+x|O z@&RQ-BA;*BOxhYB07(v%ktD}+Nf5;#g|M9za-cF2D6TD=iLKL?dm}^I$?eisx-dG_ z8-gnBnal}W#Cw-*jkAz(PObI^&tdHsnZ}qmpcR+PvyRy}Mlc^(_%V~EYJSkx91}Wvj z{1WZ7uRKgU9?cPa8-B#)q~*DQ!40C_O}8XBH0|j~*D-7HlZ+Ji&gPGWFh?d1N~O?3 z$qu>>Msq9f@7qZfwB^e^qa0WrDNG+Ip5cmn2q`_vAEN|@x65t#hR z(&}z4l@_wjRS6)KuFip!tsL2Wi?shA-~XE~Y2ZyHfCP{L5@1=5NB{{S0VIF~kN^@u0!RP}AOR$R1YYz6_EcK#uskgt z7ig`6A2m-)X97yk|34!8|9|wNr-T5I01`j~NB{{S0VIF~kN^@u0!RP}+*Aa}0f1K6 z|6fy6Px5ukO_e#!3=%*BNB{{S0VIF~kN^@u0!RP}AOR#$M1Va1H!{nNW2E6r_5WVa z?fJo;nYvi*=V~1_@3Ozs_CL1YwKZ42y_&81&8oXAk5;_S@-fTZ;C(lLVq2ln&LqW& zntCr7+F|pl_)4BR0g$oyjlZ+n}c_Ic7iv6{m@VGo4 zr_1ehb+GR49*?KT-7X$1+-_&0;sZO8SQCE7!fHXv%Bk~WD&pnA!hSmwc!du{lq^qB zlq?CyQj{#U*I_Wq5$vZ%?gBHxiO__Z6}R0O=}@eelvvClb~1F?D6%62j>TnB>O(5P zo6Gv5X>p4C3%A*s3GswnjWZDlsmQ)%!LF%pIF*YhB^2rq3*ZMEH zNOo&2&P@*d_ zQfMIUa-TsA$t>tIlcJ1iQ8OV<73yuw^g$3oDrt3CYopyO)JLcd2|l$5)x+KGng|R9 zdciK8S(Orx7WUYgtmucx2^3Vda{+okzBRd~~=deqC-&<=KIIiVjr4KZlxOE%Mrre_Af&!84RYi2J^ z^W0ddu3^LqI(ClpByD+QGGvGLSO(jpT5YC1jejTv_P=FYa)zMu?08_r5B8c%>fknY zWVujfW4NsgIO3o0zO{qT5ZY{h}sIW8s z!kX4Js1&Sh^>zC>oI&X4t0I2*7wn`0N zvZ!jb#A;gW+X@yt<1CPAf;_B3(Cx`e@$crT>sl>mH`&V57IbzMiRPh;myxBdl+5v1B|2YF0j{+vRk7S+}ppSKVlp|ay;dDYKO_8Hza@rkN^@u z0!RP}AOR$R1dsp{KmthMg-xKcqQ+7|F4oElOHD<+!_8DTe6`x~0nh-LZ}_U?wT^nn z1260>pmaz82_OL^fCP{L5qm`O`zGQODMay;ul6J+7Myev6% zlAK2!kMb-WzboWZxopMKw{QPmlf8<$-%=S(@LVdNW`#tI%Pdt)uT>S2p5zSCkhl7f zv1LA$Wj?y}pTGa{d#qK=0}vOV;34lMFNWhDvoGy{ID_Qs%D29>qPIp7&}6MZvH$O=s{g|mM`2a6+8111F?Xcf)7`P)^)7Vy z++DG#hwqL?I$iC2#1-XSk#65YyU*L+5#yrmot!t?<%{_ESckXU>-KV8zNjw}UGTbH z?ykr}v^(PVbVqo&#l^h7?v9A7qoXs{z5wxcE=1coccg=B@9OgM9gwQ4eOTCxClcIo zUz>|PI?P4mshp5qI>820IX=O{4R&;bonhUsu-g~zY+?QBbb_DeBZKkWai6!d&D+5q z9Xu5p9%^M1@nxPJ;G@ez3){Pt5t97z?(Q~MTYFb~ds|l*JHah*nYbLv-RWubxWLsN zx$v!5(f^nH?~cI#3}ZRM)EFp5y1ZPpE9UL&hXD) z&gFuV+%e8Jx9|01dsp{Kmter2_OL^fCP{L5?d}SLG_7sJvWxt93tkyvhFxmcp?bM(l=by_bu{*mx?& zufvt)5_~2Xj`0ieR6GYZ9>c4ACM(%`j3mIoqKS_C>Cc$_Y` z)78PcyL&vI9=GrEM&VvNbM_S%fkB3trzF-h%cTLfE+PMNrb7OcLy)I_c4Rcf24;d2 zp@}V}tDOgFfI%oQ5Ey6Aj0cDP3~eaA>BG;bY+i*qEe=uJ@v_Rr95H zlxTt+eNYKP!Qp@*Z@G9b!57I$B#R2E7_D_CZZR&$ncRjASYYFnON7C62!zg>$ZA^n zsX~j5nO=sXmyL>SUGRP5qi0yP*tacmliGH!aMZ^1o+Sxpgmg(glj_&bl9@M&UTi2d z+nH&x1rqQ>gO*w$o96OaFq>-9u(-~@teHfe zg_l7eQzH(5NTiPeQ?M&}zI<0hIu{|c7?%4JsK#W=cC} z9ci<|uP7Y0GomGats)rE?NkVfgozl@LK8?)caRh#t?$LHTy6lQb5WWMm`zUdv3Ne2 z(@a=p!!zL8&9E$Y9;SnyoLR%ryUSrM zpN1;Qmm5E~AN16&xq}Y*WyfmOAN987%)zv9ezx#Z8#6#hHOT_Ec>~+^xYjP(3wPU@ zplF<^W|st*rRFnJ+Zg#%=E(k%BnhN(c5i!SkD(gn9kU%`5P*}~L3hgHwg(}CtfN>g(* z7igw%r=2-bnAM6>3%`A5qWPsIsU;@OyY(}C{j6A;!=4-(J!zN@O9t!Armkd8yj4+{ z=dGe}2Z6m0@FNcP zy2<>HwA`22nNjhGT;|fKSQLF5{jD}$-S}7JF)VwO%DTvr6u9DUiiAA>SJw42^u>+y{P~{*10sEZg7tM>g3LKl$mOU4C5r)a>W_UU&b~NB_0q z&ND|p_^Il5|Jy**4`%`&y!*c|KK9vf|HY?&^M?Uv|K~UI53f93x&Kvr{rgUx{K{uM zKmU_kM#AsSe)PY-I{bfs@PpvTe&ZeA`{%b$cz$R8)#n~RE57oq)%oew-~9c}M_unb z^v*A47XSEQ`qzZY-}^|7<&TfORh?>7&QHKl=W$_dWfIrc+H{eLr{GtN-mo|9bFU%?)q( z)gNd7>i>S|;@7P&8EyHC@Ba1Ofs5?b%<#uO^HooFJW=<`i*Ik2&eXfy+r|-A?z2kmC7`(@T+oXnZt&x_@#r=pKm&v%zF8 zJ=YuT2qqKDL!sc>@P*0TaLAvVn;G613C+fbdQZFf89xcL67!D57slF1G_mP1a%(l3 zjCKt7l4z!Kv(t%e@PZJZogU9|(>{M>p!>q?^tur7j<|yHwfNl3(i*t0k8IAa4R6l+ zASn+-pPrq8xGn^|BNzOeBNrAukSxTPSe~2qxwz@M^xRC4q~#v&^RLPOuao$=nUPJB z9(QWo73~vNhdk@6v!4DeH_*Kq>+McOll{rrY2VV&29eLV6q%kR@hr^^xYH4ub#iZN zGdj?JVQzvT=-!yyaIKNJqsi_py|?UJjU*=thRxWtBuCUUwE^KD?d6MjwQO*V(yGEwPx*xV!vh1 zTjgK1()OBiO3gI8k%~HVxkL|bucY~wxOhG7Tt_74>56rEW8Liwovto-JDB6^ZtraG zh}-No_h4 zr}HZ->GkYdMA$sle|k0VnjBi+TwIZ>FxmfK>BupTPdnc2c&#Jnc>fD$KtxH901`j~ zNB{{S0VIF~kN^@u0!RP}ybuVyq{3%0Tkx~Jw8C%U@^@5ps?LU$m$DA7axuE47)=(F z=rWVHyJ8IzG;!W;rO1QN|1X4^gfbujB!C2v01`j~NB{{S0VIF~kiZL>K)I*&8|nF< z=>Ib}{jUBOaxFsHkN^@u0!RP}AOR$R1dsp{Kmter2|W7*l=J_d{gf~TB!C2v01`j~ zNB{{S0VIF~kN^@u0!ZNbB7pDzpD*&5E)qZjNB{{S0VIF~kN^@u0!RP}Ac5zO0D1m* zSX-EyVf%aR_c;0-4GpiWU*GejwPnx$U-wUSFRPrYeMij`nmn&KDc0K>-EQk^ob=o` z=~R4dD3 zlWM4^#f*?on;aUl<+*WJZV@YNjV&$KM`m@z;;C$oOGSB!mhy{vg&8Tul~8KZ<+;i1 ztR2Y%&~7|mzsL4QyLIn&ej z^zPwo9Lh;|s)z1`L3-K?PY&73#gk?H7!nKfDcyO8$_Xf1eI&=4$ce-0CRrh+6cHRH z0&Z~lU_Oxx!x5B=d?uZN(~e5qg~Td7(L^V(6jnZ&2F^D$oLwV1F2u=^hNTYr^IDFw zhRWBf)wI@mL$zsXdE~Tcvo#(%WPR;^*O=9H;%Aq=!B!o?&3ynB)Qo4; zf29O^$WYj_MC&D6WA8DG zc$l74L=HVHI%Y^YgQ)2C!*UD19)}l+haR*wPJj?&R0wijZc%FCG#tXDJD{n^`5{@D z(qqtyJQl}O_F$!=^yXW{TQAxggCOJmWo0B*nxyliic-C_WAU^o%1Wm5(juM^Yi*73 zLsoH3Ek3OmR!)91^dDiZp=dst*Oiv0=DJ-xlBk-0axj(LVYe1_Uaj+5^1#$0*4P>! z0P&qFR4p1MS!f>GP3E;_S!$kiA-9x`G`tqEQ>?Z%@-0?zxrm`MHEud5`nhPly z=@%TWIN#F#aCPIrp>lOf3l?^3-+fRW0Xs7(R@Kydxmb*i3WRVhQ$7fioZvl#v4HJPM6#1>R{d7JswYw+xKAM zXY5QsY&510ij=!ZbsP0dv*gbe?zb@yA0(;Md}$eyvq%&_6ehqS$;aaPWR73YvGEj} z+yM2biNH{xH^jP5nB-2JEZkSai2KA1Ie%2Sh6L3mB&1kLhpd$~CkIqK=R#3HZDp@#ew0{^TDJPGQNcjEM;%tBPb3{q ziAt<#9xDqSc7_*ikVV+@1iReW7{5R}a~MYyIkcfbN!LFV3XHRo>Z!5d*zv%Ke>k8F zvPx{BQ8hMGIc_QV?97U|&nSmfS;!!(%j7eZMvhYvNcw3iQ*EK$#$38YMM?^*JR}_B z*D0VK*5bbL(KFC%^#x|wd9p9Uh$>9rNHGP;!Q>cs zARt@t*qOL^0_4+8(G2Z^>~GViOiE=knXD;J#tBS-)MydF=pOPs4sgkvhyXizj z5z(j!ukBDH6P0VbOe{kg#478dz?-&+L(?P+4e&Ms=c4 z=Fy>Svg$-e=Yu>{kcW#^K9dD9MJ=n`>|Gau98exUbPf=+!dX8%(D{&ql8!|hd~|No zEJ5*MI+4@8|9`fg>@i*>fCP{L5jI?F);iY&s zCuBBE#ZVltgE+Of*_lzXMHWX=1|tfTG6k0GT1W_73=BM%D%mQ&B!yyX@R^{KluHyx zqD&$w9D@qJ9Id3^Uc#n-pPdO8_LdVy$u<=#EhG|K3nddQ5>A!Us0yP+=J!yB2T1=6I5NVZ^7kgOu7FCmY3`=oCBTw6$@)JJV!inJGo{4OJz>J`-OO4^WQ>e{?16{Z{PjQ`5mmQrt}@>mS1c0NxO zz}U&jU>~d33e;PaRY0W7~DbbMB@^93mRNPF0fc5!2;5<1r zdeV@3=^-SNGNq8p@oTCbiOBc~Qw^nAAt2u~^b) zH1Dx9XT|BVB_?a$sg+)-YNZ;1YFdgWpj6M4+eLLv(dgTGH|?*35wt*bE6v-c9OI6v zmzH|1T*HeB0cwFJxUqb6PHKN#s*Gz|isx!=Oz+t;r6|j^O+hM>o0Vl#4JpgvvV`QW zKrPFyIxwZ%P$+0wHFRdOH`1S#_%uZzp0$IJg$H&lpL`6;^gYzVka7bP0e6F|=z$5?L zZ%6%q<5C0uLIOwt2_OL^fCP{L5#?c2rU&CP6G_@$1GK>nTh4^A#QrsXWq-E2-l4Vv|Kc`fg z(loeihgZ@cGW9`}iKGN11>NE^0t$B(#_i15WuoLFX;Fqv;|XtTFWofQE|p7UiA~i+ zD8(d~;ugU$rJ^n(>$AyWgI>{MX$OuVEH3)Wv3} zjKGEtT86sKE?`rX%O~hz6DFCzHaK!zrL}HrI)(j(VLLN?*{`)e6H&HrdnSQ)+45*i zrX`^?HJzfSp`aqf~NrCB|jeC@g zCXeM^DkW{dt}sZ<%qy1kWg*HQ-=Yw>CP{R$DTK?3v!`&HYyoLDGATLFWmjVC;(Qmr z5QLWPD7I~^hkkvm8t*Th0;|S@1=30kL#kBmO%jAInBJB8tXAoVs#8l33LvJ9sUNBX zU1Z(3q_5VeVK~HrIv8T!;#e4^WJqVOlMi%!OQTV4N-EmsjU-dk;MFcJrQ~RDYm*NxQYde0*5KCBzL7M$*m6^0Fd9nt^@%0HqyHW$0HmvnRAK8Yh zoL8oJmU~CzxLanFGmf)L47!R|W^{YF&}(OA#TuEXLGInBZ-_=|PGuwz#|kH*O!tb} zot3HFRI|KVrnL4K4JD;da#~3#$EX+@eOPWo#!k~vs0RN~s8R^i5`DPfhe5jPJY*@u z)8kITeOZt2Kf2Ro0Lhy4gV<6(`36vQ2+ozb8u#`xlC5QR%|VZOkCqL_^xi z78AK0Rj3_J-0=PXGh4&J&`1CYAOR$R1dsp{Kmter2_OL^fCO#?0%-sLMkpne5eXmx zB!C2v01`j~NB{{S0VIF~kiat|K%W2W>|bIW!wug0pWXAhJ)L#0tNljpy)|E|iNJ0A zLjp(u2_OL^fCP}h4MRYzzS?YOq9U;&O_V7Y;zW^~%PCrwur*53<&3AmU~($TOZzWO z6e|@EMr~aszEW-7;!`5BS6^mhCN?y!Qb}7B_L<2i80oc4w^trDD70#-q{XR@UOi%C z&XPldWL?(W5)Z1?IZiq%Dccjv#1n~_u$E#a`|?pC#41BiCtbekE( zp>8gdzAGDk%!~W@)w_X=mySC09E9DYG23Rg#pqxt@?s2%&?db)>i^#`QyLT$2_OL^ zfCP{L5+8Xp=y*^!;&h$olH^YZO1pUZ(R5J`&)4q}0t5Ev@W^5kCA zef&C?Oegr4)5zMQLh@%wz9Hx2dg(k|$>mb*rP@_@WXU^UUt7UcSB*0@r>n*rj_mox zy7RRkBLCUmZ`)s8T@|nFdG3lHQ@U{oTz*Kr&DMDAnDr4iHRYd(M>1SyBg`lGBpB8$ z`pNvV!MN~zk@GHX3CosSO>OT=x|Luwwp4?R*zG0CJd%xk#lSGROL_s5+smNEV;&7@ zx+Vc)d^Vbir*m;3W#XCyP0NJI{jJRfD~91C8|I?O=QWfuHmscHP%gBhA+!}s9WyWs z+fq0t9dS*@ zrmg~DGvmzJs9?%Ioe@^!F+O7gm`_DB8)jigF_?6Qea1-Px+3H6{W*%IwfG`C%HM#Brm+&1)B z+nhxJKFkkI0EZIuz;I8}mxPH`8hXz4(uSuh1-rp>JPUTU^I6`cZ&dxcz+Xzp#TVjH zj&z5hEG|u8l;#ON2}u|1mS@x+S-O|xbDY#pr2>%d>bpVxeO1#_iMY5TH#b8%*PvYt zzOPA}yG8Vgdu)yTA*(1*tF9Vu{DN4@D(b!xzTEqeFr+Je_odQIW*xd9u)W48izZrVn7w!^k5~~bhxP- z^VYJ0oBB7LJ!I-b2fd}>6K}OOc6M5?*yvDT8o=}>^A_WPVCrd?W5QO$0WDJbaqGIC z7zNEK%co!mP67@#BHKi^=v-Vam+Qn^Y>i&8_38=T7-~2}N2+{=Tg;n{&`iAS5}6*0 z@zABB?uLw(2AMPk^O9ji>B`ETz_o*ek|4EmrD^DbjUp|L%m3z@$|?Py9fP?~v|RVk^KdS~8Lw&N-Fyk*?i zm{T&>({1n|`u`TkQw;pWKO}$zkN^@u0!RP}AOR$R1dsp{KmthM<|9y9QDd>9{r{V< zU@=EX00|%gB!C2v01`j~NB{{S0VIF~iV2|ozhWZz8wnr*B!C2v01`j~NB{{S0VIF~ zkigAH0N?-Ld}WF`LIOwt2_OL^fCP{L51G&spzbo&Yc8zw7n4R9LVvCoakdNXn<0#^Tv@g4+la zV+@AFLNh|~L92SNiYW^)5JZkD^*CK_r`yB2JU!m_9(Q};X9~QXIex_riAg3mSQ4Lf z+@`@`NU)z;0}IRqCqffol!&H!w02%^L{4w2%s&|#4URw*!-0{I!H^iqfg+Y{h=^DQ zF|i8=LK7F+zD21~6yjLU#0POJHQhz-?%I$UJ=U_Apx9i9*_mnatXy3B1B)q3FcuY- zP7w}MvutHeTcr66HL3)Lpa|XKaFQtt%@eVuBl`oJ6;cZDMKb+nCaPCt3Q;>VA?`I3 zRg1Vyj#U;^XLCgJqe!@wP^K~zUQ&qInG=_1wF;-jx_!0N{O;0NjY7=SY@Q6U)b?RH zdwSQkDtW8d7?BYx&I20BoF1G>on49uTqYKVs!R_(O>-$e0ab%t$OuW6Uyo;r$y0Cw z(=^nG7#KUvZ52*`igi(4_7++S^LD2D@+GZsRI`NJmyPO`7LBfz>$`;;(t@FBO`a}< zZOk$`L_C@hQgA%D0LOZ(1tuM!o@L=MYUmLb;ye@|vF-$gRdSekaWTL+5@Nz6=Ia;d z#lm?zGbvUm3s2mvjR2mrC zf^J|alfKwcI7>Qd!juRA*7=H}QHYT80w;qQWf zAQ2cT2K+I8on2mq(f}B3(x~QCEiX9%L)n2t2_@TKqf>-znv3!hJaQ(t;)M(v@`%|= zExJ;FFl-RaS~Yidux~$ht2pWsvmm zWy`lHh*HdSc0&rc$lXxJ#FJ^cife^wIF5U7;Q>PpEQ*WRb4&4HX%1f@N=AlK!U}y+xAgCjBZX(wh(;Qu?D1>0ZdgNf)8nUCluT-Ah zJTGn41!}o6Eos_Cl=OnSshNH{R=!TX2rPMrRNB{{S0VIF~kN^@u0!RP}AOR$By$Im@|La8(<3R#Q z00|%gB!C2v01`j~NB{{S0VMFkC4kTWFWj<0$&dgNKmter2_OL^fCP{L5@0j@Q?Jz2Uza-qkQ%f638X@3OxaZsH#jKmter2_OL^fCRQ85Z+^F?8lGz zyxw#ozZg%orSp+QJPJnYvI#Dm3rprcV_=}y=XATkKyQec-*ff2d~@18YLbuKGr9Gu ziny-u$6QPC#o+9zbmw$v&xAI;Q1Oe~mM z>pgxUvzYCi+c@s_Ep-k0ecdxNlgqJWsCM>bM`--msK0xBa5!)*)_Ypm92uC5ZOl&3 zEk%-n#WMp_`8n6gR5aP2oSpV94JAibBcaaTRsYynZD39C2mO=1G~a8pL%n0spnokG z=pS1g^e>K0`xgU#|I+eF|5CQkztn!x?-x!7PWeXq2GVP_@qtK+n-+R|#|Fl{6Jx2~ zQ} z>!j~w@RWCAGzwleRQ(TKa=8*36nzY$mXWM zH@3R4GUJ~Mq>rEKA0Auk9b5LfSeTlKu+~}#HK-bXm zsc5Zx!Z$n_d7G?aJl)XedTKHpfPx-P51tyUq-CwGWT^ zYi9=s$2$C{y^BLrt4UtS&&4KP-dSO)eR!4&bqz*Rrze-^#^#n+Vw;{!d^E8!F%s~ z>8{wAFvcyejdo?b@{wd@qjoSqg6}RTs)iX+)SQ6 zxe^?Wd%L(1|Kf$y-iXkV;3p)aTTGGwkz2ECiEmCW@ZK}0l=?fprW%9f+q^rHh*KKl(vy;fY z$4nxF@&YrpIlpXBH?)!FCnY1h$}N-lOl~b#LK4n{KJP?4wUFW3@>yB~4wG9j3f@AIdL^WTH4x|ZsuHORQeHjGQd2uNrwUHF^S+E@1)!FHG#Ui{j zvcN4k+dDhF&InX6C&xwH9Wi&iFXka5tw|9U-y-k-Ya8CdILiDq3-SEAJPc^*5 zQE>c*WA9eOJ-Q(QB!C2v01`j~NB{{S0VIF~kN^_6t^^KO!K$)l%QfXa$H0%T)Q{Ps zaNR-hd$*`coAOqxv|8nMx;j~pyQjUY$LGD+q&xDhspn!-0#1T!>N$I{DUy#Ta=}znkGr)= zYhFz~O-?5}kl_|!d60EFn_8Rr6bEaJO+92uwzX*~9#g-^;SiZ#At7WSRAw>49d)%r z!fv?ue66gz1p<>ZM8MUrojb>#%e!2#z(|*1v)~1e6yY)V{v*ck0Sg+JR9W_vpko9CDcrA zSV-lTntC9$#T1_l#+rIk`9uPKWw`WGNDvaac$zwsO!tCkI290NkOhi^e@`n2om!R! zOT<_C5n)aBK@Z_0ZWoP&xnvqnD9Z5RzuTpm_uKU9C;P%MBdYDQPHg7kHA{3Tbkp6!i;vUOUBeF&UHvq{RLr-|lElGaZNv%Nxk1Mb3sMc};d8}bM9{?1Lf1GS92gN7z~DOv%& zqTOBty`s3k!q_RoTO@D$#p2AlJBg@LLR>1Dg=5y#M52SE{DoeBisV>L#C_?}+pV|j}hRRKby30?7q~7iYIUl-7rbE&M0|t4+Oh}n(&{+YU<%Q|=#xNHk_Wv!_ z)eQW@KO}$zkN^^R(GhslS`|DITIg!rySuGc!;>c*k=}{Vgx6^~PF@{)j-UO~@OmMa zXN*w z0@)$!PYdxxVl>S~<2kYxMi(Pvafn4;%9B^A$ckh?T~vYf79Q3{U_C@xC{-3N<=?CE zP1y|=S4bkk6f9b$1+rj67EI_pE(1vu$LV;Ctck*+a9W6`aue}QScvhFlzkd;iU>s*FUxO*Ne*g5JMgzT6J+4DuMj` zk>sKo0ak?dOSa<#AsdttvcnQWy8NqK^p%R3EX*6;>)>05t=>9xL9>y^6N>dK9}++UNB{{udj!PBirbl*0|&(Y zHC}H`jlJGpV?Stz^yHVf^2haf&#S(`pgJ}5^|8q<^(FBKLe|?E%i+WSyd+BoFQSQL z2lvv&s(g;tJ8*ANAg@433SZZ2r6HRX1Xx|T{wocMHEx5H+0C@p05vd0)Zrw> zB&c<})}$ikt9z)~-O@uQzPLo{AbBJjl2@E$MXb`bh$N`55fz%OkkN7gS=fonmabm| z%^~?|c!%v$)%M!$T1PpZFLhaBfas4Mg_SDUH4Tbixom5*JK5aAa#@xQG8oobWJNXh z9IUaGb5!MG?M!+fxbz}Qw8%fRZyt*V} z(2|3QR86?+yv!o6tAH9Tly#O(gXqKY6evZqAhz;)7}A#)(~4JGuC>zcT>0}wJ($2d zcA{DTizO4k=Y7&g-~ZP~nEFWl#x+v9b60kCdjR=nCQ&7V%fdqA>-fQM02)=8JO z`$y!>6@A0xJ;BaqSJzP?o@FJKc$iJF@}D{dl71J>1IQ$)nLhlo!MVoA-9zi(LYCN+GRGwCBRCIOSFFr80ishOD>dM?!3=w<2HWo?z;f@Ot#IjR*WgUXMw9#A`=;vDc1v6R-JIVhT=|m&qyrgqe)R z{>peG@O80`MlQgf1t=hsq`9cxr@z?AGPD&*)aZ>AuiJMkAOdNGJ!o*dp2qumiS`$YVriq&lp^2-ZUX^iIE0c-4 zz6O~%>*|e(tMb&M6`rvuNl_eq4$4z`u93?%%jd$dl9JhI!-7^?1N)2$S^-Sn;VUtk z6t#KF&63B~=dAt0mPKL>Ee?~FMV3n>*xVA&W_j{f9Uj_NxlEjFMajaGF?FFG5;R6OHEAw}a9USF18P6F4XJiB zG@$xx+i+?RM8m1SwhgCrPBfVEW7|+#w?)HgKer93@6u>k{f%tvZ|$psW%UCGJ`>kX z`|ZqugLg@-efLv;^>J#KH0 z+l|XS*JFR}u3wopp+4bO`hZ@eA;CgDkZu`3BS$G#;`YIkZ{$Y}MLuvdgy2PmQj~f4|@vqy* zL+lgCn|z*sJ45ac7=2OYA53>W%Z7&N8=umV8uU6q1%z0$g8m(*1~;m~p>{BN!7%Xh z>897?fkN6|X-9cKu#I)AGMY)nQ$%{~kyb$s&hnNMn6?)0vodU&dWPCOlI4x-w(j)o zXmH}Jk($K2q4$X@vqmt9r_%}8owDXm_le3j&vIZX4t8vU1_)_jxRnr;0>CukZVec! z6AOIy*A^BGJvR*a`0S6n{i#9In{2ngr(J%kz=wZ)_{WF;7wW^m*MkrL)RwRIY$fjv zx!wacwcD$kR!YnC`iFnTD$EUPaJb_VegDt=3`721``_Chu&M7lKN7T7R_Wh$cJ{b^ z_^$K$wi|_O0B9@fIkkO{YXCR+AqL7~4lV)U5&#_P>a0%drx&)xE z)WOL%x)(0Rn=oOWU?)~9uH(K+MMdvsIcVr6fB)~hR0R+H7ROWM8ge{U|H*pao_FnO ztb1kck83Z~{6o!<{loS)+uLlnREt&LuZmXwU1gx+k1Cq2Z?V=`Hktom&TX}3Anv)V z=3veKBj>!{fVX>aVQsK`WH|#H60$tJLdgyC8*QuZ>|my2DxO`q_2|&;ulm5}Kl`n> zz3D5&v!rnqhCAe_|~zv_m72OWx?lkbvs?{tjk3$Qg>~6>o`g1<@%Jw zJqJ{ALatM%ru#FeHjBmKX0z*_ShsKU-uJ)l@0SA1>phPjJoa1f%m2!2KC|a5$N%sH zKmE-A$9#A3@BUp)+slG=?DmcxuXoE^X5!qbiX(Ie2f2~%krHwGJ67`>xtXP>e`A$9 z)Z+ihLw|i={qa{k;rhq*-}&%`AOCKfGyT7x+4tu6eOXQ0!@9eAysjQs`s>>)i5|nK&;|#hE(eiJ!^mOAG5lCLTK7m7V(sq4mMRk3X9D z^5!SI-@SJ7&kwboJ^hkbeerPeUtjaJN7Hxxu9|iiq2mH;=Dsa&nTgY=isKwu_C~r) z!~vTtL9Wkr`rAKw`fAYW`SxR#@96*j;pJcX>mL^0dFAdeo&B#jrr-W}^z)xm)9z%+ zZUA48i<~tBwHtt$-7*vBc2%78Xn$(DGhAACPDY14DPds2_7lf%wjO1_`>9x^@NfC| ze{|36)8ghYH2=;coxi!aCvsmU{^W1A~q_{{1d?tQIO?|t*@|Mq`hzF5!ynfRSA z{_>I2-(UUaX7*^I9gdsq@p`tr zWhTz8syLk!OTj7MS}C1aJD(CZ61nd@`o(qEgm7aq7QsKk}7V zr62y8o_l{?O}mR6+X>>d%T*a9G`nRc&R$iV_0!X?wK)@UPVpCFoNuA~kq`aB;@=dU ze|kqn{e2aG{@3sL_>+z|edFWDfBe`tChGt3Z{C(y#etG(@9_a3d5{2c%x;;9bBij@ zQeWS2ex<9lWi9lFB7xLg`_~3X?wp^q{N-0R{^reB?|fI=**APY|HW^ACwXhtlc%me z-p{Gxz#!2<2MM{~0ddT3nTg|2#mNP{dk$6Mz21?|%1AC2cnh5->tQRaWVk-ZB$sk19^*+Q|qPnk;STgTC&8iS?C5;qJe` z&%62LlkeK&3Vq?$k0)ZkUNQg4mILoyS^LC~AN%lMeoqw#dgU%sca%Y*)9E(5WhPFY zD$Z$lN6H&D5vMD3a>d8-xk&A|dR)KEe&DuGa3BB0cl_3Ce()cU{|finhrTD$$DB*j{4(Pnizi7d%(b=#7y#cp@oDPL7xNoCjOTemFBj)Ahn&JcJ&Ap9Vi znXnCE$S~{=2-^_ClE7ma65dOIJa$NaLx2YiWZ?b3bIx7smQ*FlZrgp7w5)sf^PTNG z-}%1(XKjNqzaYQngskRdgHS-zB)cUs?RFl+Vl9y{fBJ9oZtI=)yKS>A(Js zGf#ac@^^p#{Xg-;pXTmYV^KCyFmbTZN72IgK&>FZ=CoMN(X!XCXI?mWw(2G^`C{Sh zl`~i7#}BrD{5Ni$%zyKbzW2}n!-v1+!(ZJ0NB{bV@n5)jCmH+?zx2`XrIi(DoG9ik ztW`06Xs~Jr`8B86YRH*lvmH? z7xY7+e}C<_{^E_TPyW$tVdT3GMgArBe-eJxiFp>tD?@a7xNp5lZ#6~`1Zny-(3AjZt|_^|8Mm8k^Y}o ze&t{O_B;M&X8+&^zINoy$1n1_M~T-WzCAt>7u2Z{T0yC7uHvc8Le2-BAv}o2Gw+V`FeS3?#fg2RnQV6 z!v#FC1YNJVamCgW&7v%X*LNrZ4L`?6ve|y+$3RFY7VrHS}e@#XL@5)>Fu1EY5t1 zc$B`Zhk!@u%aX%~SxQq3-bG)Q6uOhXEFtg#`m$uO`{~OPfp*ZBC4mglmn8sfr!PzX z)<$2J_-h}1S<tD6Luk~2V=Ud*_GTHov=5KGFZ2ElDw>KRNej)h2;5!0;8Ti)5X87VzTOc?v zmPjSX|+P5@Oiy;{h^8AoXX{*5Lqzg5*j5FxR03PGYvgD7;!Tj++KF*IkX z$4h6^<O+Ag2lIrLYww*v=;m7 zDYVg+_O=B=GzFXut(pOi&Z{02N;AM&X!QhWwA8owlv+Lh4VJoa7^Tv}a<+As>uT}M z^$@keky?7%r~B1JZM4*gPpQ>JZLrjZr|KYTqh%_X{zqtANvoyxm4d3LcfSEXi89#+ z=qPj7y`O&*N-vaSr`%+d2PQl(^;WY_Q{t2kW6`gF|%sHQV;P z!4j7qM~U9GUOheymb%y>wtB}*sxGuaiY|oFRNCsA9Hs7d4+)~7ROa3lIsX`%Ni(Ub z);`y0zaOom-wl>I|HyWYY;eR+cY%EL3qC{HbW{JC9;&TU3APtOJpm**x zNW;bbD36woWA5zwlnewGLMWEDVMnpM?fRK^l)23`Z?N~L+fd}z1K!{in%{>Kw;b>W z3!H03fm;oDgXJx@puBAdywO6Ns|&4OI}H}P(1b#_n?tqB#1;Q*D|FGn39bF$@wegE z%$vymzd8Jo0ABuoJ^YdI7s7uL{^Rf;gnvK$yW!tL$ghNdA^bDp=fgAMcZQz{kA`F6 z{%}v%$Gd*H>!aZxNBaL1{tz?*p6L3i@OOv59XbGCAO6blmxnjQrEm_~18Vr?@N#%Q zd@vjihr-QW|GVoSyS~`<*Ij?w^@m-*-}T?SKGB$5K&F-ykQ9&zR1$ z+Lfp(Ts(;4($OAT z>_~?`0=xgWbp6NhjnpS}rOu7kf9U${@b7l@;P!u`bH3|L_)Po%YX8mF zZ}0q_&Zj&7Q|G_77u$ZaeX{Lq+CSL#XYG%-z0~ou`!DbRn*$%;|Mre=+W*T3uI_() z|92dCuH&x`yy?IP_PuxC;=W&RJF<^p2jq{WfTVz=fTVzv0)wr=BhLoCPhyU@1JZyh zA3wWsY&JP}<(Z+@(9p9%M>Ll;I|5wkCk9(X1FrP!DWX>LqXVrWw8jxlZ!C^aSK;H& zv<7FM4Yt_vgGEixWlE(?AzzxJP-_rGaAw%D zZNq#_oawh}PhtS=84J2p*s30UY+*eLk9ksW@m>(~NnZU(WtAT7M^`!~4KFO;aZYA%pqv2NKBRp|^YG`k*Be7L-Uq9B@SkS38ReLCVv05;c z&cxLOv8dfsow}MHR_eDL!NPTyU2_}{*U@OJhi|TKMs=gD)K6EZu5PrIIm2N|j+r`3u>)vef~f3NFng+7i=irVFNZ$n+H0k5s2wN}>-ZdbpR@-_AJ z%S!z?Um6aIZ9#+%KFHp)*i<)gsUnAaTJba;G~;Tn&DK7gsAC~V18OD(SI~Yq*tE(C zyuJ3xW7{{^8jMG`Dbym^BWN$ygxRkxsMXcmir0QCW$hW$xizV*HXf)Nt1TA!{s7-= z9E7dig3w!O5WH-S*+&{zs}b$n_1~gY+cy2T67H*`UsmFls>IbpU?pzeo(8EG+V+J) z!Ol<%z5lm#oeqTG6K?DJ0sJ99rw+q1A)arDD>3)Jmf8Av#sK< zWwZ@Vcb0#C81iLsHRv9TjSs*m~^qq`H_2=dCG^siil3%P=;@{_21 zY5eNg9@O30{FcCLf(HZP-{`t@;PVIGcc5qgRL7?~VxeDd|MB+c+di}Jllzuh|F|{V z@=GoG;2#GM(*K&L>X-<2lFDEu-qwBONYm{{4f3E^(zJ9^#dFYFKBwj348Zjn_{FJ( znaQOYWoh!*$rzNY9Vl!FL*OHlZ zl8d6!$(7AyS~b*Cp{%F0Bt%E9)PawSF3rwO&%CHyYP`5h%KV&i$yF!_af!F$QvW5p z9r$p!-t!EjzpEJ6N8nf8w0A`^%4gD-l-aqZnG-V$%ISsKQ>E3 ze40fa&S)iXOdoV)o@?t?k2F=TvI((#W+v&Gm`UfPWHXaZK~Ji{F9`}S&&{4$oK4%?Ng>_j!!P1 zr$Ul8EWf(1`U1O+Etu+MGelN zz|V`eId%?byvVX;JNojK!M5%(RJ-55+A4y4bel?9Z`h9HYzuXr_j98%(ANDdD(tRZ zA$Ct^=D^R@Q77{F_mrxo$iCYhmHxKw!-t#hEVHHItit(s;Acg3;*fF1?QoytMDah1 zsNLqS>Dq|!>SJh#oW_B-G<#~sb&QpZ*a>UK*4ja>1sCnRQaOTwf6_Vd#E9|DiT#yH zTLFQesU86{`d4N{3j`}Im+TlWZRv(HQ`p_r%Eci^YDs?6*foq1Lk$I7B% z?rrOS^2w$CKqF%F~s%V7tXQ z+uN;`(%d9PYTg8A?LQ%EuRi{^E5oNvVo>_6#_^3-%*Mms+32mo`VRc~Rx~k(o@S$! z#W@=*mnv`Ov$2!SMk|r^N8rcYv(bvU3$u}xz1Op`FI$O#oCljKhpaP}&1_B_#_7;^ z;J?-!UvF|go)qe!W&b6{w)ZVop4!)aSQE>@Jb?t=%L zzH*Wc6lGwA{jhs@C~Su#Q{&c=)=DWoQ}kRt=&o`N7h`rI@!MIpOZ{EJu z9(bX$A5EP^Q|Bkk#=0^)jecLz@~rp7Nn*aPyV}LLD++bDrNB~Rtk6N^HVyFKu7r6lMCLgT6r!a%sAIGKOWvCvLe^C0uw=xf~j`3dAnODCPPHgPlLJ8s`I zyy3BdSX_ZCj|5!h3|F4IeX~6yCxVI8C z-$t(AzR?zV$A%wmX(t}U@~TNqwpFvUjQA^Y^74zlA3ZMK-fRmjoMI9sU!w)~6zZqdEjA)vCoOJg=lGO$t#W6y zXQ4AmYaN@7Yu#MCU2YFdRXS^v!?DI|5rgRNKQt`HcP)JRZKFLfTRBv_VlkTA41_u6 zHN`CKmg6@Rx0`O4+5_*nLnn>cG6Z#*h;IA+!aWucw>vqh`h>oma`#9W;yVu4ZtK+( zz>)$CiaUGtAXrK25qbELA4kpAnmdSHy17en>4|w!uDOG3myu6XE0t^eV4)a8$L-j! z;icQf_P}H%VX zTG+|jmU_Uv?gBJi`q@t8^6pfCL^&9^h{f%(c4 zqVk%&pW|$=7Bo5=%S9Sa&Qt6NrXb31@}+LCw*|(^RTQW>?Ho1wcGDz0l#bl}l zKV=JIT-*~Uq4TO{C#=31xfms~|8EI@ArSsj_#eZ6i$CO#q=2M=q=2M=q=2M=q=2M= zq=2M=q=2M=q=2NrgG~W~C~To$&Gf6Oxj7W#_y5&E_^ZR82A==wgWW;ddPxCE0Z9Q# z0Z9Q#0Z9Q#0Z9Q#0Z9Q#0ZD;Zk^)LgXewx;3Xo!v2MnR5rzLd8N+1;Qd{7OVds{+_ z)#<9?37SygV#ut~DzF+#fZzW=9teLt{CD9$3IEfTVz=fTVz=fTVz=fTVz=fTVz=fTX~76d?QmVE9i0_`m#-6p$2< z6p$2<6p$2<6p$2<6p$2<6p$2<6p$2nfGP0$;J!d86il?lqw$e}=s5nb#1a$H;fX|? z-v5K)F9q;_`6DSHDIh5zDIh5zDIh5zDIh5zDIh5zDIh5zDew?dpt(5|qWk}M2HzLx zJaXWZosS>*iO#75e|F$Y2R^uOqV@CpmiM*oQ(Awg{ax+Bw(n`$-+sL9FWbJM>CNre z+7Gq;Y}1wYp|;;?D>p4ReQ(q4re6*JK=`%cpAQdo{BiIzEx#T9eE6mPk2F0S{=M*Y z$3F-Ez9rcFSK&g}XInnn@j&E2)pnvS8hl^ZuXcT);}gLjZYc(TrR7jdyz9rhzP{!CEngM-T_J5>xy|ugbJ*|n>Q!QU=3+(&gzCYjhRc(j%{nWnS+L!PAmCiy}sVmg^ zk=v*lvT^IapIU)`1LdMfnIp*M%hq4v;!4!tw^K7&bIUN{|{EJGn^&==15Cu z#!<*%QPXpoQi=7N=lrIp;6Aff5?082yP85{e$DGzfs0>rEuk@Q4DVA8OFZF?qI$A5 zOS@9Lw9B>1TliW3!k4QG;e{_%h4aD}eG6Y;gXh|l+3YyO#>f@Ui$7f*!ONd#ba&+- z(#-Mona!-|s=hf`RP$O^EcjD=eR`sJH79-JcugUnk{Wl7Dn(XwzNGlym`m`qIl~j*{b*CB1*ioW<~GtD~(t5cE2cI z(X+KGdRx`uLe)RR=B(pL;1x`;Ip_#xT|eqQKd6Lp9LF=x^P`$67<&pQcFiPI$S9xP zjtY4@M)+KHgtLYWv%WabEGi@+_BVE9>E*0>Ud6>W<_PDd$EqVZ5u>bHarp6)sDhyy z6%1CF!z&o5j^Gva`&V#;1&briuJ^6}!F_zcade9jr!bxJat^fOpH_qqN@}%!};s-AO`zJ4w4w zi`uGNw6Cf~t=_76i(0(FyhY8vEowsLM?#K)r>@HP|GRgKmnBOIND4>_ND4>_ND4>_ zND4>_ND4>_ND4>_+z%9>_kXhgZw_AygnuLaZTLt2ND4>_ND4>_ND4>_ND4>_ND4>_ zND4>_ND4>_?0^D^=C(k0Fp&sNl16efEb!w4(b0i8EbzxCqVWj?R0j<=wFM4QrnAt5 z)$}HxBtAijAaaF}YX&;61>GlwPY}KTH;4ZQ_W$93z(4XwQb1BbQb1BbQb1BbQb1Bb zQb1BbQb1BbQb1DRA)r8Ob1>N4Ouw6&O#A=l@aF>I&*A?6*AGDlWfvp`Bn2b|Bn2b| zBn2b|Bn2b|Bn2b|Bn2b|UX2t8HFpQO?m@d`3P=h_3P=h_3P=h_3P=h_3P=h_3P=h_3P=hBNdI5n|K&uG6p$2< z6p$2<6p$2<6p$2<6p$2<6p$2<6nMBPK>q)lT6zN=%`Lsb@7=bq;r|{82HS&)Q0E5^ zwKNHL2JmPQQ(~hN(ea5y{ANVW=L?2vWD5CGWa4Hdn<*KQiI;CiR?3;IF`JJ}#QGy? z)lf@?vYyf+6On-d<%F)Ts(Doz7>M*ow7j~KMTDW3wf@L@CT;zlNfq)_g=|4bE`4o9 z?Tz*;@mLJMMn?LTSRXRy)f|;MX*WW-P>x39qsnAf(~T067^=Rep-t4bTn2x{qS5|H zPBl{Nlljfa#HyMtp<+W_LkC{Ih`%q~_~*2olDbOuqw{b*nQ3fd3|w7)A5 z3^BS_e&u4Ta0mgqk16r^#BgjPc2v@RujoEv(VfOVo4KaV6*jC{QB?C1r0UY&D~0(^G}~YG#e5T3TCG%UR4n{GH-c_+}(mpt&6>s#(o2w1HGs zEnx|MJP_EI4+PuWLs(~(_wR4zeml(Jj>aa2qxX8aD_=kL_yKe6-+teGYg@b3D)=%P znHV0KNDSZWRz19n|CODDrA&(UxKz1h6mm3U)$5tkNp(fbVzy##)92}|n$mJw9{XoR zEgJ;{uc-QDHnW!Jq1ffpy1KDwY|>^j&c|+5Sg)BRQM8&F(swwhZCQUmqj-X;8 zlQ$MKH_$c^o3?3f8b_6$St--mMZFgLKIKCJD895_*f>r3u#C-4m#`zuVHZb`Xi>>f zQ&%#1w(QsRLK$0nWD@m@)y}$z!i;rYLxI_JNs!3e4+^WRky}WSQ;S8EL%YpIXL@Rz zC`?@`6|!Z6C|feLA|CNx6_$@I!?vYDu}kioX!) z=fNtbrIE~}lQe1aYm|c5BI3c;Y?maZmW6`JzcSJ%s-V`<4~6nZ@oz;L05?P}TAo{+Jux>k-FG2>d}00+Pt}$8ft3YXKm~;?5|)gCgKUXvF{akfu)1}gt)p_g1fjI8#{J=T5PKs$HpG3-q{Oc z)8`!heZRS9H`Gn)*2A>D_eaWky6^Dq4}0j&H+zilt~Yu;v$k&d@AEmZ${DuJ7qjKH zO#ZBmGH9Q8U<_4mYbJ#3p6=T?fPM^V={lDej*HR zRCP5+`!VMiP-_GXgW&yeRU?k!)cg=uo&tHDwIOGoRvv4)F+PPDv?(rl&G_uzLzs#>=4mg5x3P&bb!2aAAiR z(YtX}5GRe6yN-(LlS1ABFiRh(JHR;361oH6da&zLp)O?zkAlJ&ljKlj|s!VDEE`1<2`*V6P}y9x+f<`<@C7L;S>9To5K4z*pWS#q7ZMJn+ zo8QBKMknH8nL?E0Sf=;S8LgOyw0-#zm%hC{rmKo~&FyVx{q_en33#1TmLJua_~0~1 zc5h4kQ|nqzwS*K2j7&*9d!$wU3bo6AOxALcb{n|UlT2N_j?>#QwTFUM*}Dg1G#dk> z>y)*CuHcJFN|2_QNnIgE?54{!q9Zy`G2f1;|gVch6pcO1VM-Px^cRyf0sXwtViFFF+1Wfd}^m z=)Zlq^n|_n$C})FUD*6bCSs#<^Ou`{BYItO^S>XpyX5nKUAkdDFUMP}iS2yzPefng z_y3Q{_kUZWDc}F)%aPOphE%a(wJ`bqFW>)t^z!8Uzn@Zax+53NU*H;9>Ul9)AANeCGASoazASoazASoazASoazASoazASoaza91esWz8Ld zzR(k)md?tQwi>;zN7pm)*tIKB6*ip&3Ag@*gP-wWZCSuP4qIna&0XXoUHPko>>?~& zbTywY;ve}VDIh5zDIh5zDIh5zDIh5zDIh5zDIh5zDIh8E z;838mH57UxC`1U&uCJy86c-Y4)Fpq^!bXq;C4!(p5adY9wrZ3MKG3O}!oVJEfT-QV ztw^9i2^Rh!fQ3JRh5`^6H-Mn|@WT&13EW#SeRcvot&DVo0?qeg{K|v38#9yR62Vt#Cy!^mHA=CMCZghQbywwoXN>h1L#F%P|J(cU7kC%sKWPlQD$Pq;-3 zPq^7(Pq5u5Peefn1bH%F&UwOBJ%gF!*MHG9c;Cyw^$A>v@I0G12K{FMjf%)Oc;z-cPYUPkHA|K#u$Dhfo7Z@5=;^dx|!TvH&8Y$H5Ke3DNBw5D?zTw5`iOQ;J6}|KekgCfsBm5}lqRz|`SbKL!tbq{OI|Ql_gt z;Ut4~0?vHR)1P3(Um@ob4sU&P^H^}!7vSd|`PgiIfO-aOv# zsxsnr`un4b1h#Xny6mId+rV-zsTYl;QOuTVW%f3-y0AuCieo%!tYmjtW1<1tlyD%x zBNH*2yXfO^12ytK>N_WBy{;Z<==E|Q1l>htqYd_~w42Jt8mNqVwyVm<8>o!N41UH- z#?D9WsI$7tRC6d0df}-`Dv{XKupMff8a$2-WZ)VaJn80ut;TNuH5<#mc<#mYm2)%2 z`1$3unvNfT@oYYQ`BZlK@(bB4{w|(68yzvuol9LkJH57X>c!{hUWy-`zMM*}PR55e z<`aqH+E8KD$ebNIws|UZe7L-%kGyzgy*&ED^QVl3jg8Z%&KT#epP63+*z?FhEIJU2 zE9C1lIuRT31Ofj!Q8^MsFW&f6MNK43A4)rG?o0XgYje?=&9moL%P*~+iK)|5>iqbK z7JYv7xzkH8tzIc^>dWVjjLjd(Os^fia(rxLwwOLY|NPacUWlLJ(A1*?aRyBtjZeg5 z?$*K){YV^F)|-J)-&2*R?bdS7-0n6nUp{{B+=;d8%S)57ROVECVP-CNF85M)@#uoF zuI5Ijj@~$be(L&EVe{nb%-q$pGc&PldHihbg!)`;czHFNn%hw4PcAzfRXNfGLZRyA zt*fqgbk8lFzma+_{Zi`2>8l$X#beQPH(pv>I9gC=VkgtbjAL#l(=dT_3>I1F&uXg-X$MYDT1rmuBd14bhib@Y8JI^{;4~!Z3|3MenZ1kzpEKm7q%_s-FFVQ2VST=iel7s zTH(q83VdCswY*{!6upqu6y{}6C66-vPvsH?Us5Kgr&8Rd> zF2?JumDR@TWx4z+R_{F47D&#bV$70F-|jMb4f>xKU0yytJ-IaFNV+()#At!5S|;t& zxp&`rv^_9ed0dRCvqP*Y=2(L#TtM!O|>dxwDdT5d)|B#`ZnqL#oqvy0GGofKc$` z3R0D@;i=aZx(DeQ<|kdrYWX!|omN~tIy?^7uT1+>VDo=ZrG!eT+uD3|rD zPpBCCq-xX{e(icOgE!h534BRYdDor&p+IH-ZF7U*1BKyc1D9FdRC;wS1;Hk5#Ql(Z zvA=1?_A7k8_ld2!Bo1r6nmNt~gd9=HDnhX)YfcTu&AnU`MV?{pJ_) z;X~x~?cwbMcRJbwGk3=9!S@ut{RHr2*x@8F`)6({&XsG^*k|CKJ+Iz9?8bSs`qHEF zwmYHrK)JFe7AUR~tC_Vj=W>$k;AG6B7L$O_d{+|JIpGuUJ~3QbS?QieS}Z|NzF-V& zYQ{iH&8EumK&7R9PZVr0?d^f9)`^xX>tGk?-mI3ezlg(H*H{U(ZXMa#}i5&KcTu zL%9}PoH;o&1zyN+XTEl4UwhzGr5Qy|+N_vwxvnlbQn>qLHDjBufV{2kfmG!w)C{3G zj!ykrCZ&NXW(v1pm30kJ`wA8wbts?7Dw$O!D)2!G|n@LeyzzGS~+eHQ|)J=p;DOqG$tIud_9{RGTx8HuJ zxjmrWAtrH|9kOUe($hn8?h#qaS#W(>7l&F#awnH; z8~EfpT#y&^%|X6F4w&cMic|EE#BE9(FoHFhoqjRt$}bL(q@ACwyHcU7)!a*m2jZjTbSg1E5gWh#t~St$vtB!cyTEgo4+0iBH7x_Z+D zPu8s6o7As3*0P8haxJBQ}xSaZqP#+%D5pPQLDqS0Y*+TaV-#yj3L zTa{P4-In!^T^uL)Bb680x?eyKVB9Q*(N4n6z|0!`8vOQ#=}gs(dsBG7gU^b7aLZ=Dwso)>$}NDL zhVD-LRJ&N#%Z5yi2$~I+ZibP`uUS?*kUb!7TLb0;{JaRZt!30&_5p;rc3Vm8rJw zQwN(WV~irS&9a(`7)uy6b6^^~e-sxyCR*mOgWM{QO%F~A--0jor7OqUx?^Zbr+aLy zuClr(w;l~EvA0q&w3qsB>y^p2?t=%LzH-K*JS+K~)%3PV?L$vTO;wvw4dVHff&Y85;6)=Z%ZWw^Q@r&mrCJ8^VP-2j-KB=|<>cC1Crk6EP9!i-T zSmxd>hc=oVZ5w`IB7Kz?D$llcA3of4`y8V>`V3^RB)~0M|E<4+Z*z}=GaVZZI^xW# zoo7XDP!v%FWvm(~R=;nHom4REv(BTw%4p?nV5VYCm5eo6U1qdNnDm2TBd#rz7FNR8 zf~wO5pQ$;as}olj$ftRAN?K{6c5FF3o^OF#~>WTjoEr@v@PmmXlfZ2TgOeT?{;ftqOJRhCz|dgSw{u3j`%0=>+UW+#JD>~? z*wamwL)KJv;K^_%-w_b_RLu$LNVPonn64iFko;=m_r4vHtGiD%=)xdB742iP_hy)xR?edI{f zokMIuJr#RC1Hb7WSWom02e;O`r^Dy&AlT4-zCWT~2j)i>;8}zy5SfUmWupLS;T2V% z%mR#rherD&XyB0iItvgJMnrlMl%XBCbCI z&^qn-9=T~N4XJ|i1ESr;4spGFIfW=9mkut zAV#P_G4kLVjWwWnAuTutn(#UjoU9 zg-uLQ45nJH*xwV&_4Fw!Q1uYRu&238PJus7gk{m}=ELGmylO(6=FPj1CwCAEc^Lq> zGvN(g$g@5uE1OEJU%=9n>jdUnfkLS^)S5zX(Y9Hl5KZ&@%Qwpc2XQ7GPUTc4Up8nf z5(CUA$PiF4kN_R`n*@$Pa~cSTsBG{vS~fk!WQ8*nd|FhEd32sOs8)OE~jz;8*X! z+wQbmFEV>`xrN;n*JZ|47})$2EX5)n?hP#HI(PN@7q)$ld)72DL|C7KMRFf@A~2IL z6(L#_%N@gg&yW0_OtBh{Jy*eLalaGH4Jvj)+MBSWl~WY_a->wwk=qfPOm>gy%jpLK z4f3AYM+akr2^!_SKC$WEk8*c^R=-sopB_g#7xIh%_v_hMk0;Saxgoi3=saJ?J`A7T zV1vOGUk6vrw&wUSCO2`C2z2nqui|y1mCFNMCgBP?};Ub%kxf z;`;{OG}$I>9-6%2$`nA(Q8mBSTpu|(Ia{6_yMYkl69)5k6w_DJt}J5DrOQp^7Ty2l ztsIoS=SQ@>m9yFZYPpp|PQs*9$+Di=>VAG1q9~w%d-hF2cDFC_gVsf#+~j$Z!eO2# z1Wz=6w|-}fuVrr7%st)pXGyG4EI_w}%irYvpUL4O z69i37awj1vqMH&O!D_aQ{cb!8|8_WS_DSTS{_1W_`RV}fHYasjqEhaj)k z=+b#tr|yf$FJ4_=iTghU71j5D49Z=4 zd6N17QXfF-13W1D0N9ziQzfQ~a9?lL^8LR7tt|QezYhPirTa!7OWyxU$Y%3_^8PRH z|9h#iC*S|+oz+yHVbrLp2q*PsOrhG2MYHn#-zzth@BdJ0x-Z}Vp%w4G|C9ZHu>A)E z`2TKx{_QP~9S8)2?ZHH-v+}P8Tlckb7nSjW=(rLanTQWhBu1bRYg_7)JWVJ=+>ET0 zGg)I6YIRUAcW4bxMBoqm1b>%P1_rRHaf@J6xtjD>&A&4+8H6%6Dd$G?wH38D+ONc+ zuCByKM*5XlA2O4RaKuj9jZiL>qfz*qoFre_Y|~`AXuNGX(wUD%!97D0JheWVhZ?4M z4ReexHz%;nx0%vLRTwGR!!s?jD={0qnj!my^H7>)+8C!`*9iS0;??t-L2B3Zki^U> zit9_JT0Ejj(+I@u zzCF@&mU_z`OA@KK>@pmQCt`_&OF?dD#v?*`84JpKqPA>BgsL(){ zUiA$O6$9W7cwH{_D?R-^{mMw6sDfHYKNQN_%D)w502?B;XnAgN_Qc%GbRSoA<%xQQ z-)uVzbodt?Y-h15Wup(iY}7JNl{|*zF(i*6m_eH_BA&vJ$H0aE&*;)m z*IiNoIL2&%7i~K(t`~3*ELZ_Fz>N5ht0{wS@X=gv3Xn5!5t>^%^(E z;=m(_k{I4$-3WC~fCUJtjZ@BLnd5D0z&X%1HC@dOWPnu(+=k5oM@rMWXJs{7Hewl7 zORSHtrMb>}77E#-Z2QJ*zJ+89D{58%HIW2~Y&Zeqg$a+pSAc9mI1bK7Is6U7Ph5Bi zB#%ke5M3&i^%Tj_$~wu9#1liA36tpe1TS;oa;cE#`_H?TQmRnYa64YRagRzwTkDbPT>y@PR|N;1WK zGLalbUyK5hu4uZalIjZRLdVi$yfykj4jyKu10S9 zW2_fw3Z0l)B0v+i-tVV*&BelE;chX>kD)VilJ9bo$M4xBXEp77lE-&5$uDBF69@%+ zvom4pnGdvB>DYz|U#8|O?QV{Rx}i+!L_A1o1o%W&KR9fxgNq>_K<;W=Jqv^Yb|N9d zi8bOfCc4{T33q`bF~K<8U`Rxm=QM_GG!ZG8_!`@QAiP@n1Ug*V*6hnG)LLWnbBjwu z>!5SN=g1Y|&#JnvZUVqB36owNq4kv2-&5L10;@A!&><{me=}SuNYi^#YEfOufJ?v% z^Z%7eb5oskAvNAhBpms8(tca$k{qaGIy}a%UBpsCS#!h11!bhEOx;& ztd4HOFsy58+G0&z+ymo5)g0{+HzR=Y15;r>ieQ5j)=rTa+ZcH?kc(1)hg|_jY9}zc z7v2)GuygF!&(U-t^eYZQkc>IDUalL1#6?}p7E~}bf@>%Ngf^#I=widTdpO>FIi~iW zTlBB!g$-iJ?X!l^a8M0iAAlaM76}K9TlcH!RIzx2nS_}LXv4Q44j&naMF(PWC3bWo z5yk(lpo9_|o`^z1J|ePSi>&~GH%9k=9it;}{Z6budFyY?An`7ovGUe$vsyJzzg0@(U!dR??@i$U_Irn|E@wL-bIc?-uej-ct?U$dFvNa z2FF!h-umUOf9JRU#4T}=hsJ5jUzyU#U#k%V!86nU?`jR;|MEvtKvFBuzKyYB-yU&FZiBPD$v%S6jiT1z~fhO@#F+dL$19+(LK3p`emT$c*o4A~& z5Y%lFh?nKNZWFjmb3Srr;5FbQU?hhHUk!^riw5%jANU=lZ6MU}8Uag`l7RbBPf$t% z?juRShl+e(|5&$YUvH87dTkS`2W4Nc>7oh;{O@+qDgQ|IQ7=Oz1tbL|1tbL|1tbL| z1tbL|1tbL|1tbL|1tbL?E(&;2oNVMBZa6}+I{K-J{^LOS@!$eUxF!IEy(;0F2*9A< z0p!)MFzcfvjG^In9{%Y4(XPbYgUfZYeTR`DpMtY>Ac<@zQY^bJKLQw1J3L#!|}9%XDfBkY z2F{G;dk#ZstFCa+OvUJFemJr?67;cJxj~vpX_L+FE)T=txl3dDO4I^Z34`XAh;G)% zV1*FQ7F}v%JY% zQJEx%yUKA)OJ#w9(QS)cy(l zC=RQQp@*sB92s4R1DZ#alq0#5F@-U;0ZoNH;%!`^JOaAiIeB(Z@y&Y?^`ujvYN66-w(;Ucywy?Na6 z&Bl3dnlTIql#aFwdAn@5gY`WHQHARmM6tdJw>k&yTD%7yIl0}VGf=@H$c9N-*)*w= zNk7c{>1hIeR+bn@g>??_z26HfVFF7#*EXPe;nzMs?C@(pY?@%g;6a=#8(KD1z@Af2 ziAQ}(OgMK7R4IyG(+o0m#u20A5UQ*67$KR{w&Q5aCV!*(6QKOQ8SJreO z5zwg_8KHrQMd^2f{f-gL#|Zt3vEPX(`P@YF?NO+r5a^PS%&y9 z%S%{XWArN#m0#u?GaND(C>gLZb+d7*Kd=?0Ok`W;YOW&BP2C zy(J_W```uOXYlu@@!y}|zt7^oKfr&Vq40^w)@L*IP9WG2%dpcE+J$pWeY>wvwZ|o@ zHbX=sK@ek~?5hw3S-g}x*t}GF#6sp>MDQ!t1<}6nc`{ghlNWf}X1}~KW*z==1|O(! zDW+i}8!4ty?7O{}AHBcCG%Hd{14)#W(%8(wUQ21_$~h&ISEe8*P#`_vqAYL_f^nyV zpaynbwWNlyrt}O$jswTSt|kgD_==FXC@RE2I#_IYvq_s0Km~Q}nU|VuW{qTBS#8xo zvG=E#r!J{WY!uv|C3V3?%W=gqFE1o0GIYq`ZK(^~0bvNtV9|djL&9)iBuPSd&*d^z zT%S1kxzIF|7S2&bsE5t)uor@zusm8+aSheng2Q_X=UgBN%_ib*OIivW9d?J6ufaQu zJE%H@n(Kn&_S)z=Y;*6f3l>`lvU|>|ie|(B(oa=)dGJn5ll_LlEX{8?%s8K6F{7%7 z$Bf|uy&8<752uK=;-R8vUYZ4a=BF8KvZi;F{e9>L$s}5hVRG#wMCb?i+0`?*T)uUz zVP#y8_2s?%xbj%blI5|!SRUN*0}db-NX0UF2F1@@-{Cme<9Cn7tg-5P^l)tljlFb? zMCr(@yB6zQXsnjRNZqyA1KL;>vHGiUY=e!}kx10t4SS9?Rz+;A?kcR4w6PlEN9(S^ zaW*$rNBxt^d0I7AM=U;CcSkI?sIe+W<8{|yo^y>=0NC!XNQbZsqD!gCN5W!NGp4OXuOk(3jv*B*BuiLYzdd)RY3fEq0rYs+D% z*rn-h&%6qfvKfG@As%;%BypAVG=nJ_S!qS1n1WMPQb^c6nW0$*I&QCK#L>7fTCyLf zjuE*N#_;kWdMUd}j$ojjV!Naf5~I~}K1FJM?BS0M9#aTGdcN*3k1f;#T;B;}8wa+np6Z3{ z^X?8NNJw{_f*pi;HD>GuC*F%Oj)GFohx%Q#&gTt3?%HP1d|UR^SQC97{ddt)_e(e4 z^D&XY>v2N5T8TY`lmM1zr?HKIVwrp@TTW|d;S9-oz`vqt#U+x6nJ@ZV>IxiZ1(G1m zr7l38AMTL~LDfvyR&!v_NG+5KELuaUz@8D2)m>^5Y&+%aR59@4`-|6oJ_GI%*uP7` z05g*|s@*-`$HxitKRyt}|HqVgd?Ff~7lz*6Y7O3rxK*uU(#+%wM7V zKV-z@{tr8-b=(?odxf<@!FR#2JE7k|qa>YyI*mBr3q>^rUuUS(=_tl@@+S0ZERWyf z+vP{*o?`ZAH`Gni0(o#8^O4dQi8^pbGjDFBn`CKxjx+8*QijSsmIPC!#01^? zV2NG8Ivd;=x#lpUb~T$K{@|kb{e`JC(GjPnv>c`dnxQjSI-oK1i%x1#6&A`=v~&uE ztdW5tsD%iWDtQ~PitCD3jMOtLWp)j) zxqVRp$WIIYH08ri5n+zS5C+vXw`W$&{?rw^AD{?v3ycsjwSWjSH!)l3f064DT0>V@ zT_yL0doAJKexpU54Q#EPmo;6EVb^uAS3BESR|*ES{%NzUXJ@VH^3+q>HEhZo!QW|D`cEP*Q5y3|KCg9AjXN+ zuCwQJ(e*OIb!&EH)#z8exTe}Bjy#cD1>0%3NRJ&= znM`ZOt?V3e+vRqS)B{KP^tAji82q1mZf0S|9yjyF93+vJ|Bwnf4EsS6b4~O83j4mk z=}7R(>y%#r@I{?`rn%)x`+r;+u#cz^1>%-si8`$d|IV*;IC=M253XS;0eT_r|4l)U zwEu@uhP3~;nT9H?9BKbgS~JrAAD1`Rom|@gOZ$I%;oM+qMYv51b9%4~uR{C(F}nZ% zfkXa!m)UB|ACptm&YCraSQ)R`6vgFMi@*;*!SB3NQCazdEbwuj#c$WK-xMKRmK zAXdq7kl_KtUX(`tG-lGMUmEobO^&TqY@|`YH0p;HlFQ28wxSfwm7g`a%WPDnbK2vH zAcwi62(gVDMSx|BD83zrgQg`1aW6j4DBy=h9+S^ArS_g+e`awnxy34)Ia}nSt{LI& zwi)i|zNZ&{!(is}8xAwhXIRXr>fteCOtH8>BUSyRwcD{USyS;gbyqO09=bs?iT3dG z^PZ*PGSIh%b&rht%_V?;UGHGg0g2zgrrA@^Rsi8) zcug}8BhEQ6D~Vm=_w!f$hx#2K_OTg_5Ze}N+s8UJPpfg^?ukNNf6U^z!v0!gEoE!E2GZD@62jkFY=ev-yY zc9%6K8lX*yMwE}>F8Vm!K#jbQ`pyYD(m-W;IS+#FqO#EjDx;p2c2n6{1C>$Fc2!xu z=-jk|sAur%Q8IQuYDXLB8&!8r-NmTIq7Bx@-0kdQ0mNbrR7gX&>k1p)Rxo^qUf9h> z7Hec*p+4?Gm5pvJdO53=;DO7&e(t>pu@eG-roa~i?G^lbHT=A{bL>DM7-|nDLY?1z z?um{jASjFs#G(VSxDq=GS0K=jf#q&KUocd%P-U8=%sQP=ZC1)~b2SSTVHmhOxmwGx z*t8-x%`m`~oY2)(HLofI1H{c~dG3#kWAhT&feFZ)!4zW{3{c$C*H+ZtXulGV#qeun zg!l}Yb_gh&NxOB*g>p0+A5|u^@a^QVT_HCi%ykIo#RyBuS?_9wOmWY{VwahMoWgp7 z{|#;^TwGr=&Fc}sV21H8%yX}R7NmIzQ(TSt!>D0XwyA+#)HXE$01agF%#q*bfP-2l zZB|yJWh0hRwZ!`P8o7OG#PGUKWnMzQnrE2PY#@bE95dde`U(XYXOph5uxDgFM1&KB z!i6$%w@o{NpkOI3Q=>5G+#G6xVKks=4Vw;IpbRb^e|tzU)a6A_uyJM_IzvE|fD<_} zHypzZstR)NWN7N*no{I>4?LLsRo}i%Uc6n1F=~5iVqSw*p)hvM|ZtQp=kB9Ia}TjxmT4mL@}1cv0l%WR24 zFP%)7LIX7sSvQO#2(RXIrGXT7oQVZ$-_4?J{#5n+hk2Sz+II&D4F>GXJv<>s- zNZ@V>!2O)|@6XY6VHY)CbPO5Ymb@3y3|-3>R5Z)q9B~+isCUCd5;g>y5;ZZ4$aWuq z47h;-8C@+B91FKaRMV+q@kX9#V0s>k-?|oCf$!7UH@Z+HV;;(5crZ#kgFJ@iF)UaC0EwC>wLFF=c6kiXX&YepaLsv0kKdSi{L+$y`fY(J?Hm;5kl-I(u!55JNMA4^-_L?bGd&T+NeiKAm8p8d*v_aFJC{XgG) zm@z-0M40QA-v}M%{nGxQjx}86EO^Eg)PUf6ImtzcR?O1fQ$nA_TkRx0!79h;t#(0b z576~kg$vGzuc%Mj{}aR~Tj5*^&5=v387v?zb#>zm_vFn5?D$?}B0MhgBd<_ui94{h zeb(Bxcn(%burd(Jr&Pm!_wVa>N7$@TANKoIy^w>kBhE~#z8xXuh?4lum9*{L;fyg4 z$kWR}$CUQ}4!TJD|I>6^QI;4Y7&dpYar=V*k(v-KrA+#CfxU|0=?4(`KyQo#AI$P6 zGVFLb5al`D3OFVGekJ<&4j4|5=mv62rgMZUMX^4v0JbaL5AWY{Y>$#1&s7e^CJy~i zZvhacZHeiB55fCwNa{85&|%`bZHwaH7?lJ_w?!rlb`3J^61CrKf1Tj`+o9`0oP7__ z^X@x~H8@IjOw;HG55tK;-!>JGRznSQy5&)|0{Qk%ewvS7rxDhE`C zD2VF|MNBCQzj`6uk`Cbg``*h9mX0kb(8nVIhC?`^(OW-zcz3cyyRtRd+U)%9IK}~2 z3I#H!UmWSF0kiCZ^wdCioWx%E!4d8naCNG7dn24PkQ)oHu9khR&~d+_6@Sy=J`sy} z@7&c$cqhps;~?=|5*(&7?l(ePWahmPm}iY-k=cce?qo3IvF}wj3|PNnQ?XyMZEKki zCx7x)2^0NzOJ*l5bmt_}wpXkulq7e`5y!8&5`(YiZgF+Pngj?sACHJF0z#wv(mkEwSsOhB&&YlzoBXStz( zWys!OMX}+!yF!wm21_5I}?Q_hmL2v9pVx2_EPPQ-HlD_B67Tn zjrbyt6XqqgZe0U{A|U($uG50su}ik^cxLeo9jPYtOfsdI*=f@{*_=Qua)A}8kgstw z-kicb^Yke*+*&-&jCQg1?Tp1Q!dfQ3rko`O6w7qjzG5(~iJs-zxg}2$d*5fpR3~s9 zKfEb3lP42#PdTo$&`LHs>`CU_cUY?Bvy+S7HaQLomc}QEz0b20^DE5cX?EIEowZl9 zRFhLmRSL&m%hD{(opq+VyT-eYo1y!LZ4a*JPVN{{PmDc~o(P*%o(PLLo^S_AJTb1h z;1g#tL!LP2sC%LuW9^CI7Zv}ju%An-T|Rz#v<)F#Gm=abanM?bo(9T6QY@m?7$(=g z`Cz|^dIsNx%V(`Ub`iI}ywWjO9&1^$Jl5BidCV2dvhWYw|Nlth|EVxoWV)A(GLAik zZ>x1F#JhLkZ5O>~KG++xC&HQyo^Wf#J>ljUdxGsv*%RU1lYOGhE!h+0*pWRkHt{?W zwv5tEDT)7g_e}#N{+|$E+EdR~so-G}|1X)Oo5^tioB%!ncP1719i~Dee`;qqKLjX( zE0L+=C-MIZSq3DrEm%ig(mKzLu(9j5#Q&4{e-^(W@&B05DKDm>_uFQ?OoBBE)3qx| zK8gQFkbDyV&jJ{&o{tj$k8b`07Sbw*Z=XW9FmNJ3|KG*WzrE!#p!ftC zs$S(^54P@W1CCF8Ad3HwE3pw67EL5Z8prX8xZ(5e0eT(-R3SjPY$lg6V5t;kmO@5q zeKHTgCjq$RKv&|Nu>)?;QcQJ((Hlhl5PW&fAQL6is7kE{6`cNRS$OlUs*0LcaZCKdlIbQX$8eY0E&w+QSW>ey(DU?c1@vbu5O?tjl zQ(6wvbo9PXZCEEVl8qbqs?d6^m!-sGXlkMnIKza zL+9PY_^M;7);;`>Abc$CsYW9z@ns$me3{*7y}~m?l8LqeXHA1H1=6pKKpK3GGz=r| z4D*p7W0Sef(p3U^+cRZVX&6Q_Ysm^TJPkrzu<$g-K^Ic#y>AS|aNqwd-TC)YjY#rm zWV53{sOeDiqZ5yQ=*_-vG=-X@;!*>#l!%z-g7xKPs9kw%?wVz?KO1p(=xCm_N;1lT zp|??*z|aXxA^Vm2?mzNN{6D_=Fs(a@|Ch_(*LM68|1aB+tB<<02lxUIkahOH-q)BK zL3}J$uhxDd?qF)-kd!*)ds}GjzXCe>79Jm|X|uE0K8_{+pO%eX$lKV07xKias4366 zrW9~_2O(pFTxn0)2`sK%OVyEx;-Q8`Rm0#yB_t280q&L+nwdzRg7cRx2J#kID6mx}+fno+;qL>RX`20$1c-a%c zvDzy9*sLeog8h!=@aG8qim~5`DEpf%b>s9a#?h7N-N`x` zQH-4-MyWOTfos-CwHsh7GCP4aT?3b4 zA$c&J{XO^jdEdP?I2@I|%G7b{bxkhSarh$%|81fgc#H^5RT*)mzbi)8W*t%;#~cs| zLYK+Iqi!M3#8FjZu~+7~XE!YJeFj1&0qB?p@GAk+jQjszkWw*87D@)xV!)ZOnZ*#u zgunSW@60q*g3uL&9*|fLzi1!DZzWXL9UHThE}Acw)^+pa*B058M%OW8ha;_ zAao5N_DK*r;82oRdN#$kfHtfjTFn7MAwlSv2%C|0Ws~5+u%lQK=ZL9z)Y$rlq8eeh z)wPZ6%5FU-LFlRg+9e2`xcb>@3=)Kn-~pZRa)LxQxDO|h1ff$LB=Lw4oPbAOHAxpU zsmB$)!mdEwCNep}X%u=;{+2EO0#Y+3Ch4 z6QlL}5HR7?8>?WPG~ny&0+W$L8?UGr>XdNBqV+o{Fje&$s$h)dHl$py0gx3h3_>rhabc@P4HAe<5N{h6@qF7g$^p zkGH0ymB0%ZXR%KvT3n2sDm^p{OyqrU9`AQm8Sy?3o6^&7mSys07W|c%5N5o$Y=^X@ zU!h{0uvvQ1NE*egh41gp?ES7TtdW-H0n}xzWOrF(q5;~JVEUC5$9^pWZQn8rRiEer7@=l4c2qC{`)zqAgD^5hYj5SN9%Wb2{=&nA+-Y2 zfyU|>8TXX|#Bj$4QY`&r0TVefI^%B7dnxb+&uN*pb%Ne3k=f!FR{b|4Fy zri(HWG3n$dIf%sv+e#btfv_CslZw-7D@)R;%uwN)05*Pv5}+knVHe? z=yY_9$&mG$cqAn)jB~iv6d-rosJWI^52Jl!+$TUa6D-Bk! z1(B2!s@EQOT;tcZ>pgv3LSctnA_^hHAgojq#*@tRaoLA@yyB3kLn7{J2FcRxw4zb4 zo+shCCn?0}o)Eku)Fo18Pr?z04xjQs$tCUhz4RYM$^i*TF$2! z@-8_aVS~pcQ8&$X2eFjJyF$Q10J~T)qDnJ8aHrB*7%`ix<7Q2O^r7_C8YN^{q z9$?>?Nbt4G3F&Gj_K+-uq010fGm2&Mscbo|omFY3ic8QHO)Dh)PeBR@I` zlzp|V=_sG*P5bC^Rv_(=h!-MO1e4H}>kZpsG@J59cJpjT+d!ussTToXor0~u2``NT z7C{=iB8gNcT6sNWo>oiiD+N_gJLW3^nKLBjJteh3Bd}u@a;EKLiQl49TQDLIa1W+AU;o ziiX`W{N=QU@stn106OETv>^e8tMO`cI?q(uNU@bBl;$eNh1*eJxEEf zjE03o0OXsGL;w^VwL}19Husk642KOC&e2r4WY7@|zcks*8jycXryXu56s9CRr#=AY zTzmmY1VCc51(1`vu}HVfh(rL~NHPsf+8$wuHOM{yRZEfuLiLjffZX9J7cY=_0~Y`v zp#{IcPefe0f^>HPD{Dy;Qg~g$^)v~?9%sxF_ZLM;_uAZ6A^=JRz%0qeGU?L=2oxSf z1VAPkao?&FxK)?93#>?5)vwTUQZ1wEyK~8Anz9c+{9i0e_w*9a~VKvS^Eo89S#DQ_Y?uoNt%X)sXbMxtE|Wb*c4B_`eJ6 zHfFt%^Nlc*PV%Q=y72-nUeBb&(*s!-u$``Ul9XC9dcE4?-fxE_9q;y}elOww>~n{< zN%_{3&iSe5)_Q?+z1vvNaqQW~`r6XA*q)N|t!H+QIsK?Y@FPyg>DS^}|6~&WZypph zSw-)ID-p3)oNa_F4RcK<`o7?hasU4UWaD>39(fPp|Da@{sM#!Dq=n8EpPMBGEP8Dn zD|#d&NM`GKs0c6bhM^6lvTCW6Ns$6s^&3%ETQyLu&l?dm2{U=qg?yCMogRoMBWVUf zj#BEj_qDMl8U&q_W)O91t=F$n?mz%(a4=%=`c23!){HiU`qjKg>(?STZL=DyK$=0+ zIia{2L|r%M*eJ%>GKhC8!|zb_oz7M$V$j^lme(?QOJ6WD4!!qb_MV|bsTEH)0Drw_ zit3Kbn9tR>XOgbN%Ivgh0BkDd(bc(Ufx|YC=BgQQPGJ^5Iep3uw-%2xqg||hhhg!H zOmkZ~t3v8-nW@`17>NKV5dgiHT@!1{8*YP5d1IVtQ+{zK+>|%YdD?iy7!Ibu6Jv7w zTnci0Vg8gY59#%JaC;C$Bo-W)mqHdEm^b1+A0+eIT0?7japvUA)RJNk+=J`k+cP^| z(j{wHnV`t}@{Xk|k2T^fkM*@>9&<9VEau0SIqbE}^4OnS=5nrcmd_cmZBBC+uyUFK zTjq4^5iFzQd&_+G2Ewx0pN(i_+Y^+q0j!hPGv?NjOX0Zx{}m+`KlV}U$z;t2Pq;PW zo^W$mJ;Cfm$Fk&n@$uljnTr`+nc=tJf zM|MMxFK^MIyd(o4&i_*r6$N?Cg*joeI)|&M?fl{>2l-g$b<`FmH8x5;X`J)FbN=7- z{>VYYT730mXXoU2&J#O*`X5ZgS2be83b@?4iTQ&efLmM(U~mp;j;s@NrnvfA8$SP> z^S^Wcm+{J(y@e6_!C#AAIOqR$QnamLZ9R{8+r+ka8em)J{O_FqFT3-^_OaMb4sH^HS^$o8P!NGC zkOg;+By;>#aV&Da_W?2gvG0MKP=)eHYsoayfZr?dqd(s9@d_UtkLP<&1m$taf}KDR zgNM>?5PQ*{w>#{2xo;2F00R$RTHY_CyrZ#czix zc+ijq}E3C)-D=zr`&ZSMzqJ#=z6Ap#&pey7DkB7G2cDXt&R|Nko-6aAP|pMDIr z0%s^?hz@dFq+23Ph~d!>=}|^A!Tv1ve1d$AH`iq4Z)W6S;@?q107=u4l@q z6^xZpS7Xb_FP59T+9l$S>2H~p(U)V}F~4(G&b%?Pq&yO-lJdsblExar3L4jA%jpG# z71NhhjN)2rK@0uFRXH~ov~o9>pk{(`ALsx7nHo_eZluwDImWr$dgO8m=FnJ@WZ&nK zjNZ+q=nl*!I6r>p$M5|3uN;mMac;w4Le%6cD#jJq>owM#neX{}nqXZOWTDJn9&|hR zqyApVqP2E{J%89GMm83kIztkA^V`Q~6x^w<#G=5C!!O3H1Uxc0X0F#UdLSL@W zl_!RD&i^8K9})D!WrR&lj>18Fd&Il1pbBx$|IYc}IscPTFMlXNMfR+5%d4|<%QX3{ zC%eb+SzoH?L4K^p$_Ckxv<7gIN>cgmd3X5ywLVWtehBCMujYw!{$Jl%UHfT?b){aF zRU{q)k%}N@+8_HARKD*+0r1w;e>!#jKftd`|MU7^zyGbTOR`@VPU7DTzA@9%bjrr;rl2}djc4zac%2rL~LYVrQAe`9XOYt-xbwXiUQ)fI&W z5MxC@gts%Cn`px9JszIoIu(>Sm^`OD_muaK$sl0(eaam z-N=tS%t2)29+6N7olpd@l9xa`ii1|~FhOKyho7d<4w-tVd`GwA=kqTBZTjOwdNYj^Qh``~93NXUNQ~sFBq3==u`xyW?R0T6`ATxPI_9B|hH3kt!W%{@)Am%DsMzptABcRH82f3h0oeg?> zXQqZ(SV!R4muw~z1|FGc#?~9ndb0_14f6p&Z)oH@j{1JLP4o5Re0UGGKoEbs2IKEW zDFu&5wF)Y)dL}`)Vs)U-!6#**c88(v>f@vrN=kshc0bwJTK{lkZ581^Pjk1Ms7cSO z_D#_*D=1$@bKr%woFqc59zsT3)#8wsuo4aa+4{jdEK% zc73^Rd{E4P8X zb%YX?-oTb9od;}b+JnHBYPt~=rAuD|Te{&;U`y6s1-8_@vV`7Ko2BTAen!C1D_MsH1f(Sb~H4vo%m(Y%SgAm4FDEEMMP7ea;9*EB&4Lk%L3CSM7c+Dt< zxf5bMBM$^%DA^tM`c#en86EzNn~+jHF_f6Pvh`x7oStFan`Z2s%9K<&SwVF*u|+j8 zSvhqzv7DTmP9r4ENNVbvLxwQVo5ZoK|!({XG^j%l9p6GSeC>h$13VQtS8{N)I*aUV9GI~FV&e1Md+z0 zTB&)X4H^``oeRxzLdw=$Q+OR}trw&|0iCFk+$~1%D$~)0-Vf}`;Cwy+BB(9;DWi0y zd`1_@jZ6%RvMkU87Nu#c1D;WQo%$a8*0V+R6Ixp4FbQVP7;SV`R>b;4UKmA;3}ZP} zv#+fm2!oKVm1(w)8JYtMwyPSHqXLQivEru=fMQG2;8wfqCrQ{Q5EXZoAxkZ>o;ng# z2ja7U`0g0mN(bVDMZYcfZ}N^Jo)_#aEl4=d(oZ{r{Lx}vURZP>J`d;r|FiheN>WXE zAVZPdojK1b0i1~v>MB1pnKF6=`7uSw;#Ei$q-ctBv~dbBbRRRG7JW04ZdS@IkZ=v* z=TxLyqyE#BY>j4eBJ9aawJ&hq3zSj>;^s8@XA2EJ9AD%IO#iNItSTJ|<#dq-T%-Zg zUTt$97Xo1=DJBJ^b2hD*)m%8*WEoFkx-7?b>LLxeNCVv@AQ;9M$y27jXiC{|ggYx& znQDlFtLQ?b^>WhFxU7Bn{C#K)jbmPxyYOQ6m}i&zBdPVw%7UXdphEl4DTva>V{^IK zm_J;4$~~<`i4Fz2DHt17I&m0c=VcLxcLyz|01#4h8C|jQYI2gFfu7jXs-f^bMIEtj zn132hH!S)8|F_Z-vE%<|zQNihn)KDeU;6xEyJRtxCPByl52gf!x4`%iytTG_xIxh` z3}F^1=_D1DrPUbD9RI)L|1X$R62(((LV??(G~8T@kri8LG)uRTGbB`Os6Jo1p;P7m zZ_FE~*v4Js!+*t0Yd;M94y_NI10{*#^d49u;Aue-YUFJZm4+=HJZv`w#5ou2JD^>{ z81MN1n+>sUD`Nj>T;dR!GDSjIE7XkZJ5cQ`L&5A*9I+Mj{jeb z1N~Dl7DZ<=jm61ztQ#axftoI&wnmN5i5Na}gP$>%s&9|Aq^|OxXr_$%ELjOHebxxDWgP6oo9OJ(4ZUEG;JkdT>7icoBFL&d)^XMvOt{uodbtkGY(WzsQ;UTy%2BuBV*BD|?KWV%L+vJYe?D%33I6lBTfxdkAko zAcpnHpTx0ntA^qar$7U;+70*Vi}#0Vgr^Y4#)q6pk=;c0OF@&UjK7PSk<9yopZyt_ z^Stl$zo&N~5>h0j{$x2Zu)v#@XI5?ua~peK8gyEmsZ}lcW1uLRMSW#+ol(4s<(m<< zzR{M`W1;>$szCa7&=Uqp?NtA^+4Z;&!hHbr}dc~1aWIG2Cp;`aKE^<$b{O6Uy&t- z@(jZEGiKTp*Pp?c8#&m^r>xBt_Tb+AXmE(TLuLVex$t0ReYmpp8ScoaLd#2^6aW90 z-hMFk-k09_Q~cw8To1S&a6RC9;0QhN#}9w|t5etBWx+@O*8V$hnf&BS%ky)~%|*vg zep3A8AgcrE$;8kj*sPO_wK>0tVhXzf$1}AzccbRwr_rrG2?}a&ZhZd7o!`QIx_o0w)iX3~Fk`>n5J zCK^H}EiKpGx^-e9lW;UBGob)PGUHEO) zRH7Jx)9}`GcrFum$znS8U;KC&#F*F|FKAIPf`S1^@DRpUG7OM8otop8cA-OKR@@s# zu9@k2ZaMPlwmfI%Mdpb-Z76s=1o zXj9((C=;xgGUzU%EA~T%m-3VY(BLE(F5xqd!9AQ+X!M2-hFcgZ{*=GX=$76|Ql*ninD z5g1hTO4#}S6<$v(P&aa|RyJZTBH|)qZUev%6)|^Sg8D!KB5-vtf-c~`2yB~MmTfh{ zZ~24$U45 zru}jqwpeTxOF<`ohNY@C+34R z9VE-QZ$p(svCn5Q`nPwx(f;jz@FJOg*`wgy)HUlp2tE8=@rDwb`2Z+6fHT>mNbKWO zk|Ylfe4{GhPIpm^>7*3YLaeC1ba4XYd}RO1 zDP;M0^(?U@n$w^lS&p+M*%(Pnsvay$q9#inU5Z?s0Np2AUo>*yad86V7Rt}mOb>U8 z98mP3>EX~rbSf%5h+bD!c~{}Ec`Piqb7XOjEI8;?wo)t21+=Dn#si_SA)E@gOIMEfH=H#v@PIRYf)yDb%FQG8ydtvU)M$PpJIZqwi zXkTS#KDn5Qcn-fcP_h3;r$bI>m&8kq#Mz~lXyk?eyPJhf(oBzboMoTtdCRto z6A&MsqExJhlFuL~4Krrc(yoq2iW7j-0Wrdc>pg6;@00myo_AH=YRYte4h9$~x;msSfc z5O#UBNS@0)JKtMm{TE)M9`o!{I=>)NGh|d)s^MV0f5Uj_<>mGP!rZiBy`(VEhsZKB zJp-^Y1b8D`6;?7SoKgTSqY4)%;1Y?$6XOKTP)KEUT9w~Bn>PG27c)i+jS5RCmH~BI z9Tiz^_-q_(M8%rfk~JqkGoCtfxRJ?cqdC^Ed)_iKGomw80mlRYiGZHSEmJpL&|o9gP-1HZWkT}IFRXb3bXW;)ra!F z`gqKOx>}_vX8`-@Ep{l$k@9QxJkRuGePcV9MX&p;nb8cH?T0_g+R6jR|4#?&>!p)0b5=ZQ0dV?Yy}aNqz9S`+8B%C8(KT+{gL< z|E9e|jku9U_vNtWZtIcDC745FNs@h^OEP*lm!dl`mrw*hHKC_k=}oU9`q3HWQ~#)b zb;7NktsF#dtEQ&-xR?`fp{h>gb_zlOKBUjMieAA19$Bp>vl}HU%lElg_{ox}W?F>4 zkc3F3Lp*!Ga5&k8!E;!;H;?KOo;9nlz(%fp&6&xZPs0S0w;hDkR5h#l)psM$nJcR) zGgyEL9MMZTtn)$k)>o^wu~=T`vqDt3l^eK29`NN}f&8?pJ$-$yyh2H`0aj92Es=t< zl$JzMNi!`8C>52L;@BvORQDPC?~@fBlo4!da^y=qNYkBu2BT;kfOB)0GzB{~IT-}; zyAlcDFwWA|bW|C(p4{8o-dx}Ku(iIiy7to>O6HQr_&VG9rfC*PO-a5?{(7X2#?72; z14C`@&Mi+gBqoGR&)2PGzx)FZdoA=>D;;!`qQa9atMa6Mw3zO8kJ)0gLf<5`uZq?E z=1V>NcCLSyu1An!=hqmSC% zip>FlarDOOa}`_b`RzD5#z$VSS7?xi?$`}hT~;uBjI8w-omypGp?5riCaW$i@enA` z>C=U7c{u-1@g)-ww%n#;Y*bl|gt^OPQ?8Iw;tFpwu|!H9b&FTvZoht&?&5e;``o@nJc(Z}|=Z zXK=(e{a#C{Q~2&X%1l>HKjHZQNEFJ_{HdMQ@&6gVgILMPZZzU!==lF&2CEh!^tgPg z6(|{OY<-UZ&+-3}=*oz5_Zu52 z^PDU9?YT4>n&;czcxvY4Cekbzd6b2NULZG{=obCR*^;x*Qj&DnvLvZFVoB4J!;)ys z2urG-2$n?MwU$KHnU*x83oWUcc~y|ECX*%IoI#dkW8PR&vo>w&^z2a59SfBl-9FLr zly+z_t_nu2c@r!TKW?BQVEpfN;60IGg3y_>STf9p!5$>P7J_+%2kqgI2I8~BxONT^ zV*%Xs#9<=eko9~V01Hw0pm>V=75hnuSf?yKL3Nc(O?AXH`dl+Jn``sbr zEC%JqGy%m(6qhklmJ$agIWHu|W1ky$qcn|rWZq0iLdYgqQM!O;Q=$l{!@|)Zmd$|F z1d~4r#a)fu6+Vnh?I2KU2o(X`VrC3w;3NuCM{#n`UO<L!W&-ntO^Jt4K4>t3;p zsusU$%ih)Bkd+@7tVzZPdT};nPwvQZupcJ)D1weS!j+o*=e8rWQzm^gA^;!LGiQPp zvWSrg%-i#y25|xi1>#9!6q581!sjxg7st=2ysv^g20dspDc)sm`4yDSNh^s9P z3YUn2{piIl78o#%_!X0K@ok6zfXlC#V5>p^QX@jlWeUf{yB`e>ahJ#cd%5snWqr7^ zl#Tz_p!k1Noe(lar)9*(C5=@(8urs0lhzY;aCV1(iV%5T6V)C5*;j)P3Vg?XnAOFG zua1S{`g!=7NW<4dhT?UK?>UI*i=`CHq2t@9yyL4*w^$$hpIRmlV2kznbx)*l4j|P4_ zOkq-R4u*!MKy(z?Vz{{=+S_9Y$TPPrxJ6hfB`&ms;k9cd>XREXf%NwLzE5VTlSCBW z2Y{Z?9zPFa;24Cy#D5tk;24i$VgE{lmnlALy8Xc44Z3(7arwCGw}T$)z%6Dao2l*k z@k%$`@AJDQ76^wxWJbgbvov6aRhGz3!EOu#>Ou74fxrt0yt2Xzs1;Pe$dNmWwNQz7 zNDY7@aB_YU4dXUUw67#s$hav{IL1e&5q|_HlF*%s2zAxgAWBkLe+fp%@jj1N?kv<7 z?l#ue7Vpfh&Mh&9^$H)hVSL8BvN-fWB|$2qPfvf07CZJ#E$k-)c+_d2#hLFuN10*; zcI`c|3J{!ujY}Z!?#|X{o2Y8yJSsW1QQY%s zf^ytAp|0VKPS`Tw3Ic>}D|VV7L%r9~z6+s_0>QcUF93O*Jg*22BtR_6Oq3lG4lN21&iWA4EDv>Tuk)k2+d5zr~y9E^Oy!1-!T zExBLOZhAlJ?}ZEqji_AyuuF@AGLI_}On>FH9#@e?ko{z2kCR^jPS$E~$eLOJEjbYC z`(+xZlMo!PF4|viPdS4>BIhKR0`N~fpF!|!3qmK!iZzD+FCvO4uapY;l&o}cC#g3{5zZ1b`lOKcj{kpoQNng`$T3Jf4S)DOIMW63I?SE03HJP# z>uJz~#-WL;PaXfi?i7$= zCd9~MQ4vs)0yP086{rd*NgrCXVGAh@;5EJgRx4RB8Dgu$HN`yfLw~!7$1S z8`l%dYWh)DR9}jKj5HwGkH$Jz`Q%Q#Gy-wb0q7s)Pj({Su;}3^CzLwbjC6C{2cu+S zOF2DBEs1L8S<=+(vZTo=WJ%Jq#FA)EgMwr^&X#0jBrU0Wuq=sD3~{4{fWwwFtS2BA z)jggaV59G083}=B20ew&yuPsk5~)9Ww7x-rTlVt6ND;3mX4LK?2cn$+AfH{P zu!)B=Qr+4H4a!K6FKUTO%2r|{D1`)lj-Qfu?-vfz`ey= z6YD8)aH;7nKKKDJKE_99JeS`FWIv`Eaz+S(%IVhrBR~7#VJ| zm*v#9BB$<}Y~%Vq1|eH3E6=7|rY6M)#OIo=jLPzToPRV73MLeGMh9dCly9`{l5GAF zKl5Xp31lN^nD}lB3V6E3aniuJa7)1DJC7+=Ge_y@hI};Wc38<`7PzD^BjZ?Cu zP=X=e@3!e1#AlWT`Z3xvQ2rU{+_F@>2IA!Sw2p+@#2Nwz0Z^eX*D}!==-@yB4KkFD z)(8z89w+u_h*o?-3p*`HIP5Tx2;y>)rR^u_jjbp5wzfCdH$H5wZ>+BUbmUq~Krog~ z&rHvF3paCO0NvFqEUmn^ygK7id$O^${^7>jD)uZb7mS{gEN3IyJSnh5u44O{NNs<) z+)A>`i-hJ&WuKgHHBz9_4^3}n5jcV)n9afJE{;uTg6OwEV z=`kn5p1f4c)W}LH0@-VN4|qxeian> zD=}RQ=RLm-SD&o4cl+L^7Y~V=P7eXhKM&wpe`$OBL-ZUwy@@;H;g|G1>F%uZ73t* z0v4Z>q@8*8>^6PjCF(KHF1Ol(^FZdDMkfToGVG?FO2-+k&Ko_?;2Z}bXa9!r(96s1 z18AAr*vZJebBHWBkWDk`!^n12!Pux`#?zW$&s6+o&q?AqruDWXQV-;glUMd?;f4v2 zr={pMnz_*8@a`mOc!o?RYGTTh^{58fcwzZHh!IwD@Tkb@WMos1D`Esx)EJ)^2|MId zl0NSb!RdcINtY?Lncyn)Zqg1up!4KLr9mq>TBQwTaaw8hV5klmp6bg?*%rFiF?pieaf&~v(5412x zv1UuRA}!DqVy{9g&4tpfDEn!}ChFyz&}PkwZ8V#uUxSuVu6nB0Q}w0NO-PGz#g}N@ zE!~1PJXdU^{G>7jiHdF18;hl1L~Kp?4k}IC#YX8C2C=gv}f-#vej=YAeL?i=fUT;+Mviam&%Bj?Arc$Z5=TATGB`QJ+wz zuu-{I)AJl#+`jab!fht18pFuKvl$V&RVKX$@H{#mx5{F|URYe}omwUucP66*z!(`- zEN+$2T#3Y^86mb+He>VPxonyA>d*5$%0=N;wW&3n=QCq)t31Xk%yVTUaPQPk*N!&s z;3>Aa+=2}<>&KCQ7;6_2`6SGlTGt}#!gKI^d$rE-m_ zNv;E9E0USU)T7spu{G&zW9l+jnz6N+87I^ySEq^f$&6#_Ggh>*)fxH5)TS4~v6bm; zRdkD4?XZ$Ha%-0Q5OVkRtj;Fj{QqSrEmFQsWz(2Cke$$S_vIwY-PSWAmtYRAB}tA` zF3ITgT#D|%T!L1!W_!!&T=OW}lZPOT!UkqWAmy}-jX@_l9jd~0^Z9#zID_-jp>784 zFdzjdKYE#Qf!R@!6mcfmjY113@N5<{u!6Ip_BCgC0KRl45ZZ+sGQgX0IDyy*JgN*ZcHC4xAd9{{2;8$zlPQA3EhL*ExmDlIW>&%{un8KBG za!aJoDy0ZgBvech1QsI$rZ7ekbWUoX-oS+Q#Dg?FuC_->QCw@FrlV3UzAfe6!>@jV)DZjNa^6 zjoqox7>yZNJ(Be3qjtAqb3m6Iy|MaS#nu=A^B5mMyJwUwo!T*2lo&R_W|G6L61Fi>L54aw1J>Yuaa`nLL zfABlM`?aZS*REe{zWe?!|M8dJ{@VLyP_;YDO$4`eku3#T0CyYS3llgv|19uhMu+?m zLgfKt2HWokDUpPcDvLM={lUR@6m`=O;WR)MMo__p48>R%kjxDg{)4=t`T+$qAjFIj z8D>;hSpm9vsnw39lu?qtjk(OAgUjin#(|Om&LKy%Gsh%(UIv83JRE@MGK^I@6)UNq zrlqP<9u=vWG?=gqLOr!%=$a^x2}qj8E1Tsr3a)9T-!ijkbrfr4%fuv^`3!9k%V?mI z%xub*$dE+S8kb~dG7W+}Uj~_!&ngXsI;+AanOO~kAoYM*W-e_Y<@vHGCEREa~P9vLqYx#*&%^I#I`Ghm!7C zs1WG(iH@hl@`!O&&L-NyT0A^Nod1W!96UhL94H+B;<3+mL$(VnuWxzvf@xeZg-gFK zbu|4P;hkg~7X%)aVRcZiGad+)DWgY^71Ebg04c_#(S7Pd0JsnUE(8D*?~X~1wgbMu z9imLQsuXD&mhl16G;rbc2!M|-$|0&0c7yM}g0)_+>o==G6dFqefGBlpn>UYbjnc=q zb2yc`5CB>4an1dS(J>q0^ysMvZBLJw!p#}LbS)(^m)o}}Jh9Q7D$I+a8LG3rzW6m*Jh zpCZzswp?&MJo6a@*Q*_boi7~!{{r#nrkt9xB!8l&gLzh0Kz9nXUqx&& zRHQ(8KuHA(1WGE>e^44cOkH3A>InMDs1c~I8Bm;Q5!97!GG)q11s^LWFD91Ksy|jv zUrsD(ss&kD^G2!C@;gwa<&BA@4V5A*Y+O$)tM!ensJ;|x8XA!7M`N9Sm0Hq`O1O~_b5$K@zX9c3v_U?xLWDMg+->$`{mS-O8>m?4@NsE@`* z1TZK`jX$*xR(j-=_4$BS4mR|2Bqvpt#k`Mkm4;>ck)Aq1S<~) z&j0Thv!H4W2Os9FENY7+=0d}ZU|r)B;5k7qr|~{{(e`ws1CX0PiRR9+A6|NVrx1q% zy*BED%oo+M--Q6!@An`@FxyIpLKdZmERZx5@D$p7FYI<74g7YPLO~CdG4$=75cwZQ z9jMi7eMA{WE-0yk51>pZn~jXqwC8sdL_ta=L|I%v4?n9dv%et?QX}gSCSn(13!Ql; z5w=_i029_YNrFD6kgc_Cmrlwy)QEYZ?ZQjbW1d~=#V!bB(v*MoWUC`o3pc+2ag#aiUyuyV5C==oXXhcFgmqJ+QPBIlIt_uN> za>r;L{Mm&7*!P~g5CA*1M>z*u#loi`V&sR5pbhuI7@6-f0}jS>Wu0qX2mq~&Ci8;{ zeafpHD6I(JW*lkr#q_Z{pK{^#7=>oDVoRqsjpguNTBgY*s9Bu5Z_M0W%9#$*#Z}m- z(Xpv(TnGR|wI!$6*p8B!@?WV@kDiiaYtq@q)MZZMv9*~QC)6ibr-}8+jAQCER@<;+N<`)W1I-ItRn zcU#YhT!J~cmLxe&xg?{{b1Aw5a|uPzS`${Ag)tPsX|u{o_F{ywM8H5&R*n>s-gIz? z%OP)N3d3-7KRfDKqkih-5nnlKx)1=vL5rr%q0^y)7Ay99boztG zmq5NU>R94vqFZ6g4B{>XfWMnW-C;@w|0E3t{J;!sa}=L|@%r?Y94-XFq>!>M1OTl8 z@a2fZ_I*tC9vK`2#0Aip9}j~VC>T!DM^Kqh$rF_z0xkrA3jshUzMRl^%M)=JTnK>e z&Gn5BTk9LEYd^iAC4PyDaUlR$vnVEi<3Apq!v2G?)7u3Xs=0F3|tEC0Sg_TK~k zt>2GQ+(L*b=4BBk@Yw*9`|dF8rt3JU)@QP;?l_lz82fvE-v?C;R5n4MAsWQ-l{_}& zQ8olGAC9|BjT7(h`Zwlgyhgo_UkeK}5H(Pkg%ez^t>}mFc7}6vjYV&z8^pj)Ku_k! zpv&UV)(i1ReGdL2JwI(9tn_hgmi{3Aepc7k@soqy$d8F$OX}Z@d*mb>bV5gw&(xC# zaR64UZimf*9{0gHRkd6xA8nIsv00jH(NW|}<|oGE?kMsZs+CjFJBoaM(!*~#NpiX+ zN*C6?5mPIrDRXPyDolx{1}1C0rBr2fwG~yCS6iYqzuMBY4c3-w8e)smr8U-;ZkS|k z$=WV!OU(j-s0*^DSh{23h^N~Yo_M-Vy<~Abrlc|BDX}hj(w-5De5yzOT)BaU?f-2O z^yCZT-@hh~HAz_Nr^okAO4KJAJ zd#t%^Q9g++ts4GrqYWD94YVU`G>RA*Zn9R-lL(?h=z#Nblyl_&UGg`PppF=%*UH=M z&Kv+SIE|YWUZqeQd_Fqze+zg3NV=e%dMP{gFd0OPj#^SQ1br{lnQ9NaIg?G$+v81$ zPmJe!rw?Kz)!vL*r>bVcRw!x`3!x_Z#EC%?^yg8A9;ow2@=NI9I8dS^|NjWuJ%vpJ zIYUm6aebXi**+W) zmkVVD(|vD%VvL%J=@*6O?>ns)rGEfrV-O>QdNBPsHqhK-3OVU*GYFk}ZvL!g$n+ig zKZhJK{&}$3|1bdi4&o6=%`Gw91D6OSXZP@F7`(u8E%dvTvV|C;ZG=z|npbuUT3TAv zMXppRW3j3ay2Jgj|1^jbsF*Pu?#wo3=V=0IeXRgOu1bZi5fq0Mwp~(5ml@SdM*c6m z&+-40Ft9|wnseqlL<_?pY(K-ofQdv`pAjw8a&0$`ULaOb(4d!Q(S4&a-u`;Rv6uWa8?yAwyMr!gmY&z~VlU67%3H}^omYxL#Bl1c%_ zT2xmP%PHL$E2pj|mXm5TE}^$ezMod8v2yxyVtb~tjg>WTlqxM%ajLYuF|o9vsAGkV z>xpHx7LOIxmyURzX21H8^U0zz*0aQtXikHIWI4{3WMd>Psd}(1iACNq^hTzK3|(hc zd30w)zSJsh<3OErf1$(lNWn6KDV7rLEkn&4ZO|uwS{^gGrcT{!hT0qToDm~(=UR(v6R$&UY15jJnx9-Q@{NT5*CTQ++G8OH72bPftlkm zDO45cS8($yAvxOHtC2;v=MM(lEO3EA!j5>}1PkWq8c$lt*4j4dVtZ+%Y(tHhR|rcR zs9gZtT6?f|f7{c_zq6O7$2_~#i(NeIp7GE&^qvxcjX7l>fYGqXxv)z{OOALRM;b$O zeCq1l@c-|4u9}WL;>4@jag6&A>Ys@&$`(gG)*hvq_K2K!8wX-3e$NB%)mzxWv z*{`*7h6ZpDm*yca--NOKRcoVJp2S;22C6mTi04ZWcw@2jq*5BkYJ;)ZDBXfYqE~AG zjFcG%QU$|zp6Rnc}AkX#cfVc9r3&)o@Zts{L-7d#Wu#IlDwio0-*BuHTo%^VnUy6 zX%hF8Pcx3R`6NSN$UXOk*JCKiW&WLtL6lc+kS?ymMvab5U86iCRj&LPA_2-`W-!&m zIkqaDZA@Ki0*tLn*)fxH5)TS4~v6bm;=S)0Ldybl;xn*0;xZHg?fpWL?Y{(^;18Yf= zW0XrW`a74RJ1>_|1gkY6wOKgr2qJdGPJAjbT0;>^o$R-9{{NM9=yzL&li6uu4Nebd zFbbb_#Pg1LzN!!1k%{M_v!l~#9I~SelQk6jDd**!p|+YH z#Zk^P-NqPg^4U<%mkXh|4fJKT@ohAhN^9aa19F<65OJ?o_TjOKzkaY&3kUt>#%u#I zr!vP_!I^W3K*?1@eKVOb5`%RUR;G1{R^qe_z+UDhEy0de`fIa}YUeO_%; zmKI0@TZ*}^*}ytP(-WaK>y0Il8WuX3xhg;GUtjHq4_bhm)(P!lxrY0b5DqK}Rd8^mu)=B)^=>Ej6oj zg!!>lZfU+sOZ=VU%8itT8NrKgrzVzy%8gVA7l7G`(ZTXlxsjzR-6htz%8lHq(g?i? zSX2_)8OD8Vqtp3r)n>5BS8k*}SG5(sPgHNHUZo)#nxi&Uc^Sa)u${U3J5^c-=$($< zRON+$q=vJNRLlAQ|7z+he{~fopWQpS9&kP2dcgI7>wywI@cPg9e@{jT_^n_5ldr!0 z^>1V&1S~8pFU-5Z04^{9qXN6YK0hun09W*xfdR?DMXEkV`Y)7%gcvjPNL-)`44`eB zE--+^Ly3RoB*`h#C{>toM||Zetq>PNiKgm+s~G66{85JSyuqI`DC@W_QJTnYY1&S1 zOErzF6Pq$Kf5hMv~=)10VW?^{WM;uNW*Z}*%cwc;dmiqv)-CDpZ?PLay& z5NEGQr9DUd6z&J+TAJPTv-@9m@3ZWCmOZavzbn}5D(`bgcqiG$#dE|~FEbV@sdDAT z86+GX*UQH}Kf)6|y4Ax>V2-y{FN7J5@Z*^bEa2H-4KF{=@Fzc9+g#IMuKeaeuRl#7 zuNLp=X`O>sgq4j|6=8sKD%^?4X9q9Jx@XiEtj-#v)S$*t%C9aj!H&i>X<4LOJRdLU zf~Z{CAS#5Fn*k3_*m)c=tRa}|ZT$aAr=;Quya>AOh(WjDj_0R-5)I=vJn(Ea-bHMK z+HzxlruMS7jQFH*;+t=p7$7t$!YjZvk0P)lBtnd!34kC?u06|a&#MO*i9?J~b2sci zBX_@#GeDy~aG(b9I#5U~VCu{DH0U8$6IY*pVfg=5H(_WBx+w@cPvS0ctOsd2NS1Hk z-skacXEFM>ce~O4?SAkgnSI&o0&+@Do9jIQ>9}(y#z1n=KR^tOB;`R-(u2UGk|-cX zovMJ>CL`RXV5>TOg5&=`EB^l_;{X2_5^wtH%GS2WeL+q2C~ZRA?~f@+AMO5vg6IHQ zIhs`|p13OYzCD+rf@1l$H=dd~ZPLrAXhfT;MFOVVYS+we!IY%-(a|KevtA+P4k#q@ zax3)$Y5ANMuj2;#Sn66SWXhu*&8+N^hVB%6xrql%;+cKFAW_DyH3+fQfngbr7_<{5 z0fkjc0}t@KkODd-@I_-OY(awhJwiOK03|8#6TOT68C^ay!BEsejY;Y1%Jwa0%4yvI zE2u9gmQ=<9R#06{ETEpN ziKUINgcUZfCzjO~4^~uPx*#h0YrMlVNv|fKwFaQe7S{k%`eak@!_b7#)v8l z&5Wgpkzs7u*=*x_3&S+a*2)~BTo4ta6=GO+;uXRfRUT4T`? z{Qp}~ho(D=DoGStemm%4J;JTsC{3dt`2?miW{z@5P9RTr;l)XbzKvZAKAzAp$6-Lx zE7>Z?^>IAJi3*j<$hV;c@?ZRztKiaj7_jTDPe}!pAsePs1L+NRSnxw-9c7CaLbkTJ zWrlLNKq|E47al)~j~C7o3FT7J5w$?QRG?ZaQY-OU>H?{}XfgSoWFM>&Cle>o5TQ>~ zyfvDE2t_TO9=XYJ2&F698ns|ZtB0J{;@E72A~arKijQX?L0eiq8|hg$%sc4(q-Lg! zFN#c#%~5Tuv{Pc=J*n{GE{KW?qT+(6z$KixdAb2uLtsP(OOM}e3o~<;dFSZDba8n2 zvlg-q75ym-*jn2r886DR8$i;B!W-ie-`weIUE8ST!FUjj79AZN}DSW}Hx;T%jh`Co_(z&sf#QR%hfJQ=480$5y7Zxj-s%jB?9}(cifg z-Fdl$B517%tIg`7h0KAi#4`m1G6?!e;SdhyGfke(GC79lvr4=}$|YzY zBM|b#Pg|%!dVUJmSkxu7A-g;Ps^Q{X%Nc5`X)s+7m5G|Bx$+^0MJ$DCL$Fjng!MKd zd@50X_8Ca^&&&SA#U&>5rhfh5VOW zw}hrhMDI~rqRINXF@aE2xDrr+su~TVJM+&pmYNz)c~^;5>rw?#A^!ijrhYK>_P>H( zRexUp+3$T<@+ZIl%YXEZw}0hVbNtD7!Jqu9=J)$i>Z>r{3>-x|{oP^MP1pMf3It1~ z!6#C~^!boePI$L&VIl^7a?+L2zYnk@%D+QIX8`Dt?XdCwu76{0#%t8;__eSw~p3=yI7O)0u<;Zbxy@>K!IX+flc9J0&Da0%;5&Nx{2zbo?Qi_b$nx2!yXDg@pC4m)Ik=F1hi>_V zS`l1FZuvBOfD8erx_mmyDuzvf@;0YHr(IuJ+wxWpN-Q>9Q);2{ni2~Q*pygglcKa@ zD-)%Tvl_{CHJL20Yi_`nWNmA(bw}2?P0eM^3v|aqz)QDt!WG@3p8^#*B}wFoX*r7()Rz-WDjAFw zR96$rDe;VzQ&$tq$vMl4$%~1l^vci5>C1^FO?i)%HE)zEEu}$KTHcsg+K?4lVdHvY zS-n=WqWaSD`I%pjS_3U>r=BI2L~|MxB+GHOBpV}XN!5d8NtB?Y*MIY^X|0d@S0BFp z&0pjCxaO^9-K#Gw&n+!C?>c>))5ke|91LoAW*f8f%6P^NPkT`es~Hlx&FeTOWNFU8 ziKO=WPwxG`{xbj0!?%C+o4GG@etEv(zD)OJx-YZ*mx(Xyckute9t{Ua0(rozJxiMDjYe;oHE_|L z0fc~Dm6&&l zK^NE0!_Nfb9O!BU)e6?ZzZnmrF2nbuN9mp)LoY~wNS-keX+;oPw!+WQ=|EW%20mB~ zAvuM+LvjqDmIbI@RDkL!wB%zd2jybTAEps%O#pM>Km=3V5>JGpSugI6J6fI8vT5$V*xtDh<_ z?YRee*r`dV`c93oCMOu-VW;+FeM82-gl`ac`Z*>CE%-6xLD~{t=a`Riut)wKY;Gn& z7m+kE(clF`q39?K?MK2jl;+;yfEFkCh9dkNoC)ZbWA|3hvg*qnU=>Nz;8wfqCrQ{Q zFP!!;p_nE3y8V}7^1$B>D2fQWpDrI8!A-<^vg^ky-O#anf1wu9`l7jt!u#y3R#C_9 zJu^My9dl(JMG)r94s-;fa}W72<&JN52yPJ?(>OooH33;QX!ZnX0Sq7a2^GOpX@KyU zXU}W)a!)^kfi1+pR_bJVC?3JAlN8wBXYp{1hqkfrICk$-JIiO)5;U-qA%+6;ojUPu?0*^GZE#MUG_0?p+(i zWvRED<@sf3LFYL4YAiE#$L>7c!GD9w{}6;`AOkN}x#sQCM5v7V)SyIQa=3KhA}ef{1{+ zT_O4-aBCq0kg47iXSSW%CTHNLmr#^F$Y@Y@t32zgJjYILYm}5buiyh=N>TWKBt zA6rGqx+2ubvoBbj9L396cl>|LjoAj4ERS?+;LKA2X4$=Aj~Mon6ba2G<4zh!-9$3% z^^uF2T{CycR+0`_u0>fg9(JjXIeBgH@=+z}ZdVp*%*#CKZfq8L?$afEV&eM4(`BB; z@&8$4w=+M2<2Y|P{=YsZB*(=Y#DW_CCj(t7Rzl2Y`3cxcj;*8;J)!yK?9__RkhSyd;(1YraZL{JrAJjg&P;u-BpW0xL}AMi#5S z36{FbjVx7Zgx=&Rjohiy2#pEb5GudR-Kx!Cp|9LXeXeRNeBY|xP`ye+dQ+<2Q01jS z?ugZ!sXd}V27`2;GYJN6hzXEd4m{vUQv50JmQO>+m2Y&fyp)Awba-& zP*)8!DPWHK!0-|7miVv{1*6RK)`20$1810=7aF2^)C>&qGlAnZl;?X<7m9NNVKR2_ zm+NUjz{@7CJ{<-zl;{K$lqz-(1r|znsNJF2#Ki6%J`IBxcuc8&N6Hoe&u#Rg1SHcf zybOi(5V=yp9F8X}hJY#LahMW5xwb0V#f>2)c5%j^UcHy7v+#`= z%MIq!=DTEtImEm=h)xWP=kKeJ2ku@wbN45@FTN8opP}UtFr=)c~+J} z!7S0Q<{Ym(Zs08J@UVivN$ zIw)wtKd01Hr{+8q+Nl7zx@W95!RRDD4%svM4W|NNr$RdRj<_qcQ~StW9l27U2lGJX zlgBPQ6#!o__#`%ZUalv`dETi2PTh6SsQ}DiPU>vyQ~*M;lU0;lcohKCzqs@NS)Kpy z;{1Q@_Z)xUw8ny(PzIBHMw9Y6{ysWB16T2l33SrRoe#v*0JA(Th@{^$lMHl;jdhPuO0 zc>s67US4qgkh)`};4I6*bX&RXGKEb%5CYZp0~u}5CqJV4qL!${EC>AnO<~P8NpCj# zI#kW+`TbLSqi7fL!5eUv+;ho|EH5dHs+U?8Ep{bHOF_$LqtnVZu4lukz*}Gko=iw3 zWkqJiws2DMcmp*#KrA~mLiYj$Whd?`c+y}A0IZ|M1Fhpkz*DxhNgKeA-T=iIEd+DH zA?Fel{m#)G{eklESgdo8o@E>qhPkg$Y;`=D90PJB9QA zcN~9TH-bAY$O8iaK$No=bB62eV^d|#+PQyu=rD(J#CeP#*muVY{mRq%WVzSa%95tTQ znz?;%(*s5yHOd7O^#f8wGzNL7nYn6`%Xu6QIE80zW7yjbVlUeBc8C2=H%Jgr*kg=* zokJlS1s%F~@3F@)njXPsJwy%i0>6F0@8KD~m}{|HhtF^wb`*f7I*)yfI1{y?3jvt& zgx6=|7$^N6%X-~`=#?Ot`#}uuFnvxtd2b$ZxEN6{XP#Zl6qWu4@YSh4XQ2N7VU_0+2>k^n6||U>w(TXZ({<2^jTT z&-o{$4ltU*Yn-ksz)=7`TGtLaXQN-x(_#9F$Un>&QL7bR4OGMR>{ky>)J-=xAVbmdIiQu#Dg?&l74{9#3;EyM@ z$NB$vS!qWBIEnnFxlo$vOxt&=G4RyOH=&i^75Ne!1z<^fzVgI&rkquAz#EPNkm=4| zv7i=3v`dZxP`Qu2QvQjK0+5R_jsg%YI~OUOh@${>6o6w$^cKr+dEB{ueYM)AdZ%7m zP-BKhNXg|HlpO_N-%$WcMl;DYC_Jih`Mfz>->H3GZNe_WZ{cH_vy;t-O-XV<@xK~N znaDMgxWFP9P~FAE12GsF$bboV`6vdFGntnh1)xO!@@Qa{ylTaKuyJacm_c5%q{?GX ztAYj7R&p#UU!}*RmIQgGP$8Scs7~_p_{d z^x~Qr)ys_a*jKsoAaZUa=kdyvrgv;rMkm>g#TI+Ej0Zz^*705Yl!UeA!h^iZruV1CFpY}AkLEt5>22<_}?M;-r?)R zeJA#J{Tp*LUZY;euZ4vf1c*Rk;pelWAHv%i&doIzy_GIv=rP_?7NCSbTQ7ua5)tPJ z)snUkR{Eeb6_d`O1mzAtM3lN4`7!ay%9(83BZX+t3Avh6Jn&wawEgbSFxiQ{HGPO! zF<>P{Yy15mCDvHUoa+w`wxg(*)r{ND7qJRyN?EbJ4~^*(5D~!oe;H%_WH!PJ5!@&?gPdw zj3pD`^gdPqxe&>PL0mr%KND$~eALJ~z1AQi*5ZhP`S|wA6D4#H)*9Mpx5Ce`oPz6` z7N%e|bo=voe>+Y(o?!)-EaspIGcYuA(BR$YciUw9!pfY*Q?jj)tY&&1?M7(|I%zHl zJrLtgn;osLa@-LLl>;&p?6#O%$PJ=?%tAJI$Y&6ni!!*Gdu!E?P1)+Ou*372`D)RO zjfU8l=!M|P9fxi1Zz~@FPH6s%A9J>IzE9If@F{&c9Q41_-|I$Ed;?!r49D{u@N|P` z79;V7`x?H;emiKA{oeO?c5d8eVKi^w*xBj)!Ofi=`tLi_H*bDa|9E*Bv!7~XNMX1~NCy>) zD#c%myMwT>7YLjM$8YJP2Y=o;z~>dkEp&N1fPdanbE&!0XFcQX9(s^LWPK{g20a)Z z`8~#lb-{`MACo-cZ8NTK%qPQ@T6pYX@bhv8B$P9tQK#u`l`}FMJU$A(iwr+_`r_Vt zYa=Uaz^yR|o-rW|)3QdA#{$F-CV4cM=jNB2joht;<@&s;YijDPH&gF^_q(sZE#-}O zuYX&>v0Fq*Z~hHDT~;8gqobs7WM)*){6X@xGh94e$~<#jh)46c<`%sA{PO(bazltm znOmwb9`bLF2krPAm-Nh(y)gQ0P^bPJlBsA3xq{15#LIsX$XYz&wa+L+RVA-!EI z&L;={Hx4diFn&0>gN#9H-^5{NgEQ%nOom`Pkj~Bl1<1VO7`+o@>x^fyI4h8A?M_XC zeHHdi0sHACjJu%LFs?Q^6~{VgMaYeMr?$mi*q{2{VIab1?9`rUhF6(y^v>qm$^*sj zDznke#d$AR-P4t=?c6=ftw+1R2->N)zM9Lra)0#R_Quo9UDMqio|+UKSn!NtnZCG0 zA`blT!2e_R3kUumyPYmIOYf%JY)Qw09!Dh|8XL{>qM2G%y;pKw2D&{QIPkv;XYRoN z#i~S@T|j(gay()Gf_|-N_m}E8gMY>%5by&tXlh6^sH6kA^FxR*Ao9@iWC8pNW^W1; zW`RLBOj`pO{%(wvt%R<>Jm7ywM^iA&k@#i)IO^2Ns6*;61OImFd@0nPqfXrtdV2vk z%;JspaOnHzM{yz%-9=n}jyg3-!ZxV_7(XC_w8HA|%(&Dc4rVnP-1&mRtWMJ4D#GuO zE`-Z9^p~OFkn*>qOh;b~TwHBx8omVziJ;iRl zJAfM;-f^1exzL(vNB(yJ0OtXsBQU5pN2!tjfAtvS&6_#KT5=7~PLo?ecoSG_2Rk;t z0fBW;VvT*$8-9{Ae7?_DX^Hb1#FZN<`%yUx07n7P>iO;1Q2?-sP_MKOAiO{eE+J_g zb*ry9^~wuDJnZ5WWm-`!PS!YS<<>0;QpnvdpyXB>>0EY0gPdI!%@r=2A{9pbZjO)p z6dql=?21>gL{2*XJIOXK9?Q5Ips{}YDwm@GV6OO8W0`4{tUW;4B_uBY$q&~y*YuYw zmy3K57@b4P%yI>xoH{5B&o`>spGlJ$S@(=AI-{1G(U@sQSIe(1|7AN0fc;=rF0&Yh z0^@+n@_k1E0BtmZIUEH5*UAIjgu_ht@M##lKr}GnH%j{YEqIN#;Vmk>zSu4J`B2O- zkxP20j>X{Z3s)rRfUqjT@XQrfd$uy?q7E*P9!Knmhv%uoc?^E2b+|vhGeEYU=tekB}y;GzBAn(1LKSmReeg=*!)^gYfMZi)MT{Dt^q$idb9n!YE>7xXD`P3RRI)H%zlvH?Hp&?C|s?nVQs= zUq&h@rR0=ihBN;Y|AX?09l(t_O@mwQuJ6qMOEe#tf5v8=qRL1wvQiY)NKhs5H{ z|1N53&6)pQ)Ktf&F+3&z5^@fk7`@t=|F5L^Ut#IFryZ#%DKN9J+6v>oJR%YR0Kf-w z!lR510svdKe|UN0|J`))|4O+!yZC=)9nBmCKR8VrdWbP074b ztqn&3;3xnPfQ;iPr4v0tcZuR89dr}iTLgyJWA}jKGxH?7v3V(?m{F0&yeyMu^E~&d zNP|*M2%@Zp?q#2a?ilURrBS5`XO?xi_*{Rd&x(Rs+ewO)+bm=-}fY zW}$sP7dku7%P`oGN}LW)22wsJev?<=^a{Dif$`1sk&@tEmO)6;B+Hw5B<`12vii_W z6X0IXg(KbfvQ238I;&vI~x&4S9z49=`*t`j#8G>B-WJlZz88CNCzI(#sYrr!Oa#G?z41*1S=w zv|R91X?bH}X=6ELg^lZpW%Vb-it5WmPoeBbW1XvfawA?ygkUJpY06nA=y2rajiQCs z_MGEB7&T=r3%s6{mP9r4ENNVbvLxwQVo5ZoK|!({XG^j%l9p6GSV+Xl{{|g- zMZU5w`IVf0O>LP?9bz=92LYjvnSXI*pI@lyDmUVs|Jg=h421FL=$PE8O2`10Z$#E% z+}!x4jk=9LN1M4@wHfSql^dzgRc*yN{|EBpbk6@pyN%ju@>`TS&GlYMJ6-mKk=;El z8=ytXEI3gVoDQX+D_&vpN#Y3aB-@a}bndN2gy^(LfUfpxV@3s{GU$r-wJhtL|GhtY zgrM>8lVTzX`Dp=pIfyFbLX7@PucljawlZtF*^T$1-2uxZxFVh0RVYJJ>{bQ|I=71>F8eolAFWhIc8vNOK;gnjNpyntNVu z4vzcZasNB+e^_@2)Yt{pd>>qQfH(TxwtypMA&ZQ&jsnp7?VtxuBcP4DQJO|Qc9@Bx zZW<1#0s&i0psnp9Xf5`MYB6hZENbB}M1F;|#Y98YO5jLN&aa%F`T#=E`r^O%@i2%- z@#`7=G@Z2w*kvf*uMGG9==$uO|J%$bUVwTrODM8(91chij%k~c81L;xu_tD@#Nfo> zhYX(w9m$*Kp=bpp@OXAC0SE2|dnr9p2>Ypj_%Q0QN97pdUfBOh*hz`^5plW8po4M# zzg0U`gm5XRHNredP(#H??F;1tkekSgimf>3|I#k)<%7&hgiOV;XxuIBqNss3727C3 zsSLnP#Ww2B`QJJJTgdJj^H{o>5tyFS+_EqX{R8=YU{vcl6+vvmF2Y)P+Dd81igG*hdp_i9CWuff?p9Q2nP zv&@yAC_->Y>FNN!WDffb?umrvl5vN{{E^P`e>MqWeE;tPeU5TuX{w?(9pu}Q76-rh9ka_nON>mP!wnU}xwLFlaskNkL zH1`GR%6?!;H*%XxYKiV~I*xhB~9dlruL=?{!Ek_H>v7$i4EI3X`Nw@S-;t1~~+mO<9?yW|o=t2p)+N+Hj z73crYUHSRk(_Ylh|G%9M{cg)3JD@oSz_8!(Qy&_Xo4m`M|3Ck_MkbK_*7-U{G!my7 z?pM!T{vgi({{zHr@HQD*%7_%taLT}dfPWa%IEjWa;hLrO!weJOWFc1@hzE}-@jT?} zyi(gM|Kk@y7o39tl9i|tur<42uvO+Y8rlI7N}FMMV6HnXw0*+WOZAj zU(GpQ1OHG+>^o8a|0enWH>KLbF#tFQ0LK8}7y!h!Y?mHXr~|A{C+6DpGE5%$yFnMg zOH}LQ^0DD1C;}kt`tb_+Kylwe?Aaao?ro)qa1vam4WGRE_QG!W(ZFwqDIEHc1tA_f zA;eMPCMA6S4uD?xAco_i;kYQsXHw~hD6XHofYaw1aN02dTuBCijEAZ7|Bt#!UhJbArzY7?vOk5zLF z|9>hAv=O+5{3sY5dReA&@DyO;f{-DG+EQxWp(CUCilmx@(i$}<&zF7`(!^GLjfDl&Udms>`Tuj7BPW;$m>P%zRv#*~ipt~s z|3CWZRgJ=bIsbp>|KFnHZGXSo62-i~$}Z;O%&@*%t&PRS(rfIB@H?;8z@2*O2FQiI zTIJ6F-}(QiEslXnB%gsGdcfuL=4^eZ_Ib5AwFKNkNx{JR{|_Q=poEHJWao#9!oaN? zfJLr7sWW$lof>`hN_;R247y?3!Ume^_&_q&RV7rdm=Cr#OaN%^sQ|?#{Y*OOR)-f? zdOQOCrn_Cy68HcAyHnTxu5-1&NImeEPrhfk{U>k##)YF4=|nr*kNJ62b(?4Fr;ghw*|{7 zX{KR^!NKobNbEds6s!&ffZ<@3Q#|lk@j`wzo!+Gig!{W|b~`hVRK)N~Rv(&aT=am3 z!>nL^)y$$dK9*;T9R$2l|9fLEVi}*TZ){t#X>V_q%bv;fSxfk%!I!_;u+&FcrT?1F z>fHX#*(8lEa<&)~O(w}nVo6eS#FC~bhb7UN5tdXv5iE)Hq|B~kb01Y|UQ0pk3TZ2u znOBz5naN~HH@qQj)oXW1TWZ!PhdN%`(&QjGw}0Ie^w%XFXz9El9hK-+EJHoA2T;C3 zrzJ`}`R~`c{Zqe+pN#vyT7lRxPhHtQ>`Xa1XIU|MF|m|hwpclRIkBX{Wv6f_y=sY9mzIpEv!BIa!mmxLj|PgA2pJ@#VVBPlNvVi2@gVQkF7u6y`RY# z%WyCBR<IIJu6!uU`n4$n_E0EZ!EKA`o6;Z=Io@%(@O-TGIju3fu+t@-Zz z|NU>g^^ISF+IHdA+|sQ^-D}J(H$d3BSY2(a0Lq3R?@VpTn9AYeLcWF<#*YRs%VaBW9#8Swtq0q7Q`%&r>E*d75;oMwf(Oc;T zF>ugOH2N`ob(z;p*bDIo&_Cozo3;;D`cU3U9~z$=XDQ^C09khYM_afiH(tPR!W1H1XQKlr{Ek_ zAN7ukgyYmq?Iazfl3o;nfA{1;`P~=(A&uEpg5_fdf}#T-Df3jrcX4#GU5u}xaX(so zzBt@JpgBl8*#i;bkG@a(jHRQT5(#>M-Gr1Gs(keZ$XSMZ{y5)d6k`QKgM`UDxxjx+ z_WVw(MYVoNK?H9gE@6m8pwmi|ndy3O`X(v5DVZIr+R?C|-XN4c>>7uyun(pL;+v5o zHj^WDIB8axX-rw^y!`;j0h}G;u+0JdbO;$+FM#d+AohL%j}^+BX_O=Mv6^E}6(y=L z*ElLnYD?wa>9d#eb`L%5B)L~WYccFQlt_JqzG`D`TwL#fhe49y6+AZ9i8sKbKMecu zeWVzElbr=F<7UFo`BPIDGL&K1PX<97-?iAptA{ppZVk?@;gDI5jh(QjHN(|lTF$MZ zv;&9>Yv{rnim--@#?jxzgXE$b?%{|)SddqoQUYf+;(@>cY1pRQAJvj!j}&kuhl(8m zf+bxY0YyUMh#f|9ceXy;r0t%abB}*wRFdh5#8WB`qX?-Ar^$XhVA874@k@DZ27(`{ zRohV<2VFmn;uZwKw!|9V8RqEcD0*~{Ekv@(-ct}76DzWjy zo&7wrV?x?qr~=iXfKt#<%^4bry&&j3iHY*(AWa9!^6lH=11!rjcN)F*z)yoD_4Ysx zRp$0O+c(QN0nEunCWiRa_4vr@u{8LLW{+{&`kH|zcHuq z0RP?at*?J0hXytPA|)dZI2>*GLGrXSTs&MNHKE{+0J8G)zufu* zg8?9V`&+-MTC00?G_|nYJOY%o1{bmqIb_tkbqh^?AwZ-3Af>P`GW3lFLJ9|ULr+~2 zLaHc2TfM^s=lx2Q_=ej)P7sXpnK9D)L~;*_SZL#f3%me@vYY3jO&WK|VOFwnvWH^; zU?x~H!xH|4RMy-}-Z20GLpi+(vH-^bAT9DVOXQE7Ekdm?Z_~ey0f2ge+$N=8q|Jxb zaE!DiYtKkqDwA*FwN+i&KI}|6>9Ng<$%~1l zv@+>XY~W?A^b3 z_fOvWr|@*jQWn{WTg+yBy+|M{2ytuO!NOaJYcp1<`!y*0S@UtgO?kxTURqc@Fr zrau1myYDxX;eafSUN`C^K8E*X^8p3R@DLJ)h$VaE0mA$c#Zo`_eRk-s&)ured-c1^ zbBoJ!^RH`f>er{9z5W5}^E(|c9(IG4loehAXi2tVFO5X4ELj}v{VuRMyan$Q-l&=U ziMO)4>fL|z;K{=c?~`o3pMVl!d+o!uO>g7Twzu)*!2@q~?Sqvk54JsWEp0Sz<(~4J zZ|1H~ef0WP`KK&+x|4jW&)(d=KK1DJyLhM-bY0g^($+A6Q2r-*| z9G;SW7PI&#-s;Ns+V=XxHQqV4E_wUS?DeS+Ur&qH!+s}t$wDbX6GPJgz^p?8#@)w> zWP8nfva$Z-Cu`{U)wQ2`pFH{TT1#eVMe#la!xqYYLL3aA$b?V48=vsBPi8)$)SG+; z%`M()-0|vnmggG4#_hei_0H6jkMS5!27uZH9vKdTI`OKMqmN&B<&E9i+?WdS{`N=oO zCtvmjSKj==J5zsn6U}bkTe%NnC~_$F*txeC0_O)5ay#k-9=|{u*KK7w>F000_s-O( zR7Vf~&HDiP^7y^vo7D>wg4%&Vjy_ybf1<0KSozwUo9|40XjX34svc`%J@?<-cxUP% zTUmnC8~Et~bui)k@V=N)2uNsMQL^3j`udyc>r+2?y@Z$5CA_fna+%kOZ=!HcQUS(T z0`l7QpmhLnCw)VFUaCZzm?|A}lX6(6%EqP_Qw2EEMWd2hyJ4={R{&Hhhv_wCPl`^|UXoqGMw>$}!Y z10FtY@d=~*#oNPrBd1YJNS*)r+2^ z{SE5_W9IbzbQt?xZ}4pVVC!*YYY#nbc6hC=zxmBKzjuA=_TOk{2e#aps^QF!4Mx&< zoRLv3?8K3rU?@n@-^nN5y$6r(sda{?N|5^dmUi2Se;2fAc1kX;7miq)*b4 zYsgk$EaQ6n5KA02&A))6)C580M)zeZ*TZ?RMQHwfi`Jp}@J;QVsUOj5B)&MXF8Oe$ z=F1yUgJRAm6W62HPv875zKUOc{kg7;Z*w%S709AB818n%Hg(=!xG#4zolN`DE$?}K z^wZFJCRf2fm4Xs9W7STn^UzLX?W5PzZ@lYM(d+y97o&Yym(M;`?mb2~^f3h|K7HY% z{__5tZ$rxYs2gEKxJ+Hgix|*s|+3{i@BekwX!pEEI4_7vS=KW~xXPGZY zSHQB5d~{*}R6TNcrztG)l>Ku57)qgYxaVuf(nlG z9(xSyS8lExd9nRH`^$dw&F`4vx{wC>0Ffl|Dd9_72arx9{0poob_(M#F`Ar};EG0C zjiWe3|UI{#1o$|X9=yGM0B;CjIIfa?L*1Fi>L54aw1 zJ>Yu4^?>UE*8{ExD)+#9RKlJAEBBi#;CjIIfa?L*1Fi>L54aw1J>Yu4^?>UE*8{Ex zE>jOU`~PJcE%&Uh2V4)h9&kP2dcgI7>jBpTt_NHXxE^pl;CkR_J>bs&N9#9N%JqQj z0oMbr2V4)h9&kP2dcgI7>jBpTt_NHXT&5n-`2TBv2tL3cT&8Dt&+2->^?>UE*8{Ex zTo1S&a6RC9!1aLZ0oMbr2V4)7>j8cKFPGotay{UB!1aLZ0oMbr2V4)h9&kP2dcgI7 z>jBpTt_Ln(4>tn$9VdyVH&&vy5V-8WRc*!49R=lq6q!SQv+JoEL6 zYsw!j-&Xb#W)3oWfg_>*k9mpte+sj+l5yt);F4o`G;|ufze0zMp9-j|hw72+uE&c%O@9y!n z^|W`lc6R!GL0@}!K#f8&g#7gE&fg3gG!YFnfl)rjTg69?jPj@ZdJ~sPyq?{Kn?QU^ zJ^-tdU{yd@_%fGYVQ#dqf_x0NN`?FDL4(GeD!5_*oVx*}-^yE+vQwxu+QX=WZ*ZzP3u?^S6TV zjlIQIwUh^L0r7kCy+tv+E~m3pm>r-%N&b4PYCvJO+pCc8ueM5sbJv0f#A7%$FL9-+ z;DK$12{=tX=9Q9v?wS$`zg5UTP*+;vw@Uu~wUvyQG=Ht)^X+b{TK~BkkWTtZP5Kq< zCeK!baANeD@GE6=7pg!u@j51xzjA}b3zfDCT)C=$zzYhHVWJ6vD^~mGJs_WSJSO>9 z$^h{6l2C_?OcUz`_%T|H>cDGduJm3Tc zN@#amCI7qww^DSe-N!e1j)Jseo=gB!pIZmn|h8)c@JsFS9-v98 zo@xv9DCg~Nrtpn|sgYp#jhf%R4?pfIha+QL@NY86LIfqKZdg{{+} zxquG&wy;$?T-Z~hEo_+rcNq#epAKWZ&{U!VTZX`HG}uzTk{N(2=NFkq(17?wvj+K1 zVe34?v+PyKZwXta!UK1L2E;3xLAX**@xmP?n!#4tp~Bp5UkCYSuyral*sG9l23w`V z1G`EygDsQKf;%eVW6M@GgR9`^~Yt$;0`{!{$_T?C1zPhhomL zyXW#F&o$3kqBre>^^{yjg2~KbHxp?B_0!2H5FD~GaZyNMHTgIv#MlLak40rRCBmCk zUW#xsABc!io{bBO5++Bp$#o(#=w`Uh<+@>}3z(B&d|wI{b?88wyf9nS&tdh)I%)ikk7NkXkb39s^w11yN$-=~zlg#&|YQEu2E; z;yjx`zqp7b$}-AA-dLB4%z%d(Rd#6xCWn)fxFR543O>q4#8^6>K&3=>z{OI$#HP4; zP=XjnZ+}1AH##&u45k7mVoEy)_BItd7@A;5MyJ@3>7gOEKQz!gJv7DoSerUs%7om^ zy{klzbV4|j=83&yYX!asV#m`XgX7b>IZuy=RC9(1R+B{!Y>o0@ZPA+wHi32qVWN9E08OK=A2t~xV&P_WcEWbwkhXxi-9_$0$GQ_frW*r={0I72l)pNH$$1a`8hWRtNchZp_OYLpE5=LPH0Zq?I2m=H)>9P;;z>oEx}~f-tDJ2X^~Hv zUlQ*a*Nu?-#w#@x0Sh<^E%Lb$O!fBV{wLueegqZ#ieGlT}Yvl~+Em?nLeV z5b^t!4^}={NRf+Ym3epl?YBE0>c%^zTEHV4g_UKQC(QD~Q(vCA6m$uaME z$lGX_QjRN}yS}s2d2t-;2V1VC_3)A)r$qQS%BNt5zHIxDd7D*d+elb@v}IA-(`Me3 z(K|ggIyeHk42MQwPqArVhcz!D#mCK?LQE0iKmk{{fK5t;GUu*8xXY>R#rxIJ79C?J za##{$JlbEXZbdd;Fz+QrGi_2c-sU7zHfLiH$ArVIy;_m7YK7Q>h#UeQtLuK^>VC&&9)W zK0Xg8LvZ#0jcQ}2H;(yyT}-cY5~{kG-XlFo-hd9V;3h$tb=L>4cPiIoWzYsDoJM_* z>K+9h91)$rT{b7SAr{FaQkvC`v9dDbuHOd*->w%t37gz_iHDOLKobI+?!hKZj~wCr z5}RIYDTo~yS(%I4Br4PH`tI8uN}a7#RYg;-cNdpQiiFSSm{OTa+oUuoQ||i7CWkVu zgPM$`;p~mJdsf?ENuENXEw6j*&~89=V~v-WO#@P^!5XQ7-QA!n-O#MP5~2-FnVXeK zcYRZn^PxEQKh{*%kmK1zBXtX{eM>g*8sfEVNMa!Y2WkqvJuQm~*yx_(w5(z4o0?AMBXBw;TsQ*RdEAh{?O4r# z>{>4Po|I|@PMg3+I+Oe+*zh~yu0PUf_7cW!-4wTI-I{NC+&I2c$PVgmJdUhhonIASqSr&x`n`EZRZBNdxV0S6lqz*@#u z9kKN&dz4{-s>P|y>U!wblC$~Td{Q7cj$DmfQnIgo=jYsifW{wwDk&6;w` zU4JhOUUujeYYa1>kKz+40S*jG#zV(qetjBmO-WR6j90iC^$fxChm@1<`sF>&i#0k@ zXj2P+zC3B{d#%uAu&=vY4xBf5pVN`d0D|C*u*CJs2bb6H)-9J8CQ-+kg z4>#f-&?dSz9xw9Bp&gAR$IpsH1>u!*n;(;FLk?{uuXS#EABj%@V7cnqKgo)1{swKB z=Ae^C8um+^yp$73&$}@prdrndR7-@5MbhYqPqYyqy0PRyda#^Ka6Gt25z@fW+L(%H z1wikk`~nAuYx7^~8sO65)6Q_(HzpK6r_%(}!OlytH|2y9cGs`(bY5)I%@q4HJfTo) zK<|9C*$Y1W7>*-7;3lc4S@#2Lg~Zlj2S2{J+D! z%D7jfd_zw|5-d8E;#(q6_p%UaIN#h5NyF?o-q3Tdfm;*gKJbMwd59k8C9$Drfs4s} zv-(1eiPA6}WQSZ*{8|bIz9{|>I?Di$NN_C1MX*1GM=)Iz;qg2t^~Qw71oE;CJ-+6K zsKl*Krq*L9r%*xQQ)L}RDk$IUINZ;L#!p}BnBi^*BHOZC!)HB zDN#g+$@TDxaF+ZHB}767QL&PudIE$9<|N1zy_|&^*-}G~AI9<`oQIwi&Vp|3%?&cN zPre^k4@WqvX_^{)iIP#HVQ2#xx`317K29wLl^=@z8&!a~D&h(VeLL4!!C#f^9uAz#0Oe%T&>bT8aO_j8JL40!zssgQq?g)8|1?i z-HztP6f#g1=pjkK-Vr=U}v=Ewq?w)by~q z0UQs;id%q_->8;hCOn89^){S81(^vcY`=)flLc!{E`da18R$qScn|^5Lcp4`HLNEo z$|P^hEJ*YE4>M=~*gLLcrFkecuwf1|>ALY1H&U zZPFO6V3UVJLQe7#01KNcB!5Pjn7;!wMYYsnv9SeFgqDur!sr25$Txvirz7a`i3T|x zZ#bnIlTl*Sh7a*v6h$GXrOqQ~$TYI2p}W=J8br0cksXy-x1tp1kb&Z1668yq0&3K8 z9z0H@4|7#O4C4O4ZH}Y$OjScD6S&bc$oEc$hC+Q)Clhy_Obkqn4znw(VR8O6tbL_8 zWPB%4Yd6dniVkC5RNW__B_uhCi=#$C`~gTOW~?$efVHDXVA8reC;&OGE{Vl*473vZ91JdF= zx7*hY zbEmXkA08l206h?N=gUNDOjzMZMCh4_KF}res6z&TP55o0Pu|eHu7bgqU&Ycq+7ih? z?D7t{r~Nj zN^WlS#~Yhi4(Pr>X!FSg3v?sETDBW{V3?vu*TX_0BF51~7KVf}FbIWlFE6*^e_#a* zC&nXjoEl=lQWVTv(Uc>x$o5XM>R7<&*^A~2JQfLHcRCHT5i|<02o2-RW%eWtCknmP z^*MhG9r3nhlK>p zrC4OD>eLSvr7QMc|e8O2>1N9wj@daA~ zz&6?0;}7-({ItF=cYV`UhfROtp{ov8R|dBwAodBy6Qkz9WMDB$mLpfFs*At zV}>Dt3k`I6%)d=O{h?(gE*1l#>Ie@Dib>l>T+-dKD&f_!w0@e63#u!N6} z@cBSUXrJMIXz}vt{sswOR?!Cy^;05RNo|;4A3-0wOzWwzj>W|~+JK)Iv7Do?8o(D1 zOGYWQ_K3bS;B@N8*w$6N$w2?^-i|)4!{^}=r33?)Bqi8hvJ}|Ba7iNkrHfcWcQ{-M zSixY)#Xw&^FGYmEvt$ums{8e;Q?b{zH-DY#v%r{vSe_P2#K`Ymr%)Pzz=B*WDlM*^m zRiKZ*w!4a8cZa^KNdKr62>JuvCPxgLJub_(5B00n0r-Y}0x6XH$?EFe0@Bh#I(4-# zKbcVXguurh_q_)uwc4}<+A+xwB16#a1 zM*Y;Z%E#aXVv!9o#q+eP4=o0ELs-=J`@zgnp}}3je_K-sV8O=M8=xH=u)DC}rnt?N z#Sg1&xtk6Hnty92qn%%~&QVTUMmZR}qSWg>z{=FERIu???RIc4jW#gi zz0R-)WVNLO?=OWN*(ikiR}?2HM3&Y03GDHL<1U)55v5@B0c^yBu`rs8G_<2?_xsTA zApY$~TWZ_UFF*br^x?mepaA;ikD_goNKRO9L6D7+z>i{K#r!xypbe)*+lM>Rub@xO z!T4xS6ejAQMZ3CTS|y3A@HHJ0Xq#%uj1-=zdt=4pooi{>y>%9zkV6q*SI7bI{4(Ab zhT83UE!OYHx1AD8sqX)OHRFDDi3w;liU37`B0v$K2v7vJ6oE&%0DP|Zc)V~(;PugJ5D7GD<&U+R5rJ;A4PL_nLwYJ(_PAvmZW5nx+d&r*;7#6yl$UY(~hpXTrI3o%iYcEfpR3C>vWhPB1W zlDa4WlWdh_39qU4^q`3{obin(oA!42n(pfGnT`eQo`8F!kB)_hd#Cyig(i2CuOevW zrMdCht#|D^d2;vO>6zZislB^To{a8kI(ZWPcSmDW(+U5no*tOUqulUkEm*zP*4gup z2N-L$ClmO19vnwdue}qcFl_se@UXAHGuRol-?HCKz6Fyt;Jl6dXhGO=$Zt1Z9Z(Mn zHlerb|6!VLEJ48n$W?XRa15OSW2f*k4=gztixb+8epvL7pO#e|>#~SxJ!FJmHExmA zS4GkTGph#%!y{->NnaI#WoYZ7h&uoO1mph1mJVfT<`e;n07ZZzKoOt_Y!w2J`hu`v z;Ki&0uWRu-b5;!6d;D!rVS7JaF@R|>fRJ3u z;OSZ}p!0uBh;!+?I4;NhA6EAA=l{1bCz-l4wO^~9tocw)d-WTu>#P2t@>i9o;ZA@d z&-*;B?$^4nsZh#)TplU=x3ZAy-L6LG%bi}wS>`*Cu*i>6eT#Re_xd|e27{qs_mPFw zBi$p*5^Tf-j%SHa9pTqoSN!r3sbfZv7p~nsbi;EW|MW+{{o1#m|5f>epa0v;Sm1}B z{`nstsQdPw*A0wMsbiV$7GE3d^Yy@K#b8&_M|+W=`^=J*>K(c|Q@%roW(TB0XLHrz zj>&6*Xm|VByWa8I&o70TM+2YPx#!Q`o_^KyKT`dry?^!i|9s>xnIA6x+kfk_gXrXB zTSreYSoD#dIydX;h@FE+xRLIWJaq;-R?_RKqf5X2lND}PQ}6p9_}9B@_CEYI-+!#V z^?j#*^|scQtTH$tb~-_zDv z^pTx9H|pxlj0J?TbUMGXPD{en;V$|37sclLk38|b*cZ-zsQXQ;2R^o|^~B*DpZnR{ zep>-Qc0-micA&|pj8 zd+#ZG{lJfJUw+lU{`BG-vbTQj#1CGSeBEavpZ-T(b|?Cf=Wp-v`HMcXQ|AUSMoE&c3AathsOU@+(V*adY3HW%-|LA3FHT<&XZV)UkVHr_Ob{I%4YwS;E98l)duWM@DaG^BRh4j)z#^oTpFBdU(H7++Quiu^;qir&-?6} z@ARu32Y+4pb;bMkC-?bh7iWHU!<}DxZt}uCJ$JoEm)!+CaY&suV#=VQ-6K17w(07u z9iH{A9=B2F5Pv$#wJ&r(=e>Wi_^FF6|8$e9=5E)={`K`w{Ic%F-+W^4uio>`$(sN8 z)N9kaI#5|{J?#Jx=_EiMyGM5FT%)VA)ZagxKGT)ovK9uW=0l0&ZC^eza`W7A$0xqD z{;9_vy7`T*CtmpD^k={K{rI)zzdZENX9hT39q1%Fa3?|h4ya@I$WEO)U7b`YIFXWU z)LEWByPBSA8~n_`ogHQG`Pxt1-8;TDKlZWZ9iQpm_qwkLzjFMt{6*LQM;qxRKsN3q ze15QIT|>J^cIwpX>Z}R<^Jinz`RD`=`OeJDxBEYM?2$*lcEeoeCwF}E@3S#>WcrEo zXTJEAuSE}j`OkZ=`|z)Ob=jEm&==@wCtWCLX!poYof=)8k--DUme*|1ITk-O)qePF z+o#{2IsV_$2i|=xTYdk6=NrK{xvqKS)^}C@$n|ggKK8Y@{qSZ@9Y1su&_jSJ>*&}% zvQwv8SEqCJz&tlKo!`)pw09qzTsyNU-un5wgJ*yF%Qsg0rk=d^iCFYCuDK62?Rd+X z)ervaJ@5O(k92jwD|aE=(K?CF7QfviJ9Vmbbq@PG62XX#I$cu-&a`uUYQFN@J-%16 zk6-se?unPa{?DHOlka@yRoq7(_~F6z^Iv)F%`bYFu1*K;(Ss)2@!LJJQ>Ri_Cv<3X zd1chbcKSLFwRg7<`c8HI;7f0;{QgwyUGKW@8e!snZ&GHSH~OCUe)Ge(4{#rPtZPPB z2U<0-lz?N_-Jsv@k)1kTU7c8w}-z_qk_%=wo}I*ZkM3@BC!; z#Gar0!!JKvbC2_NZz%ul&-c{4QCA1^X!yGI;TKev-6K17Ji0n-T?a?~(S@13;n(58 zfws2e9pe9d_XCdvyl;Eo#J_(te&1i-_{f=_ceVe?qpP1>cz?$m8^8SPe+6|^S`T$_ z3cYA|(MNXbxOH{9XJ-;Eg9q}}S#CSi)z%>mvM*`eK2&wnyKk5~_Ge{%-OKO%XhYwd ze{t$Njn@hHw!Od1t;+@{11=f5P+!qUcIs5<>InYncw489I{fL5j-g|t;P{;0(ojT>ZIxDls z#Ie9+K03Y2(+kH&I>-P0!`okcUaJ1nN1hkB=QryQe(C);N&ozU(5^2v{N$%^diOg% zhZboAEq)(16fhiU=p98L*{M^etHa0E)`TOoGkG(K-lO8o^7!&-*Y%!{eC+&C;)Q?t z$bY{6tuKD-kE*`%zh4gg{i#gY@%ks;|7WOW1z?LAriAH0mDPsODg4Mz9ha`otiLku-V^#VE%KjUfTY_e>?Kh>U+NWQ}(xawl(bDHvQ!8waUMIbZzlVKl-gh zlZ~7VOtN4J8x1h9I<}AO)N$(SbPV}oM>!#1o%lq0VKy-#-Q<1U%D;U3*7BWS85G-J zeN)44{O?Vk`t>7E{@vR?efL1*lONV)gOdT<0oQ{D7^*sUkL=WO=<19``%jBq!OlE% z4jfIa28Sl6-g1w4@Z$@A9`C&?`m>IKJy73%=DA|3W(5eR{qPF7>j0oxAH8OeKeAN8-m8@A69rlF>71|7WiI%*ju@>Mi?E z@16X_uD{%^aM#^%{QdtM_&fd;-)egO9mDU08Av;x(8B5iKBq*!MWH#a-J|?TiE?X$ zt{=DHOH1O?LQ;T>s*372k{X83iHX@;|M<;{d~)>Epa0Qy?Jxh{u9MCGI{5je4_x=& z4C8G6$A57i)%Anr2((hu1J^2PD%(A>(XV=!K8NF?>FKp}U;c;9W2plZ12VtzV)+L< zpZ|*C54OMTdrQ~9e)g|CFN%Kb?Kh7e{p`f(uNsb3d=ks{b>mgpAgrJ3Yt{uG*{Q?o z>WF->y-kGgEV(t=wQ!K*h2cZoj!%sHfA9_GtKR%K5B_v}^vV0~JRPo^dTaFM52j!5 zy5WaUYU-$qjA*@vtgLmDb;q1XcIxcZ)sdp(F|oZb{{utccyKZnI-QVG-*A2X>@S`7 z_Me&k>5k{`|I#a;=GX18-f@)xnF5ux9O7D+>Tq*Bx^n*{O4zu1?Fq z40oWrJ0G1xeM4M3A6j;Qv+28Uzwftgulwcwzk1WjkKX>%vHxM-G+r~{Z+_&{PtI^} z(ADwbPj|4WZ=hrM$WEPGb#?kD!&9?i8+BHvVh09>)+P>p@c%ycUh#a*V|U;5p@UA> zz1&ZkkAF1#^P2y9qwwt)JUob&vf5h!3Lkt8f+g8*Sg$GiC|@1q(yln+UxY{3`JoH* zfOC+Zs+w!X{;Pmd?p(J@C0nIo+ib zTJmsZ6x`Sc{bA{3vi=Ul()d(46j8T*h{tz*^)$$c=t*NjLo8E-2Mss^3-TlS18}F8 z51m}xh`_A0ZMx_-K>gBTj0ze(T()~NFM%{s;uqo2F1jF@T&LBVZ-RthQLV?__}FbY zGA~NaY~vEY*4RYen+^eMe2d2UU3PF}Bs9Su9vvJ(^o=<}+0hZ!h)YxuqHwr3FKTPv z6GX=PR1fH)OE_U*r=I*p!#8q~C=bx!(VTv=b|IaJ;G2^XsUhAnkF5vdt4ztj5T_3i z)l1Ki<&$H%T6~n2Tr~-_$?zJ^T5r{Lq=@7X^I{hvk}S-V4ayb2E4es@q;V=Amr}iB z7`3N-Hpr)-Ln%Cv8)#xf>G%{B+K9~2PzIWR46c*AS{WQmrCPjfSlN&)n!$Ri%!->M zLrhA1Oyq#_lF(oh9zsOUmv^SV4R)$;ITa#t6;F|9u;cuSe;)2q_Jh|!-l}9;Myq(# zj0v8-=$c2F?Guyh@Khx^Tx%cd9ZdIjT1XE4z#nPd&kxgo*p1+C@xgjYH|(hE38LL3 zc#!~Y`-6>bu-+AL>A9ir|IV_XGZo*0U-X9}KoOt_{LUlr`muKXBD5!#%Wu3X$47#j zd@9^(#p#!SZ;92Owu|q^9-+11^O|ir^^N+(r*JZ!*0=4kco@RbWAVM{uA?}N z=P;-IMl5>t;yZfro%K2_Ouvt=8sQ?mYDNtbUV>9s--ZY|A6k7w^9fOmrGzA!hQK}a zh(0}@h6iYV1q*W!gykk!JA|0Icb9a*(p(#wgIu1aIb#_P5yTVjG_t~jKIg&LJ09+^ zkLb%ZSTR_4;#SAgyadZW>UtuCC(&Y3gL+{JIz+B<%)xQXbaFQoQ-JAmG@O!~8~yRd zCKIn73;cHJL~3`*SJmUh8jYTD1xi?5dCKF{+bCaFNF?BbczW^O(|++?Y3s>_mtD|O zrU;ANdb28izulT3_kqht>%hlh&x*d{!J!u@r(v~mrhqsS+8la3#Le@_&Pj00)ZX!j z@#cohev2v$^FLJyzua{XGIigr`;WSh)je4Ep1S{2_cD3`ATU&-WpL~Z8hX0WcT#Yg z=Cxb;!N0_B%=7}l5`m%@0G50^CcOY~6CZ!+1%PN(o?ZZmZ}p`Y0MZKp)q5V$hje-Y zAk0L~Uqr9!&&S5qa2rk1i?2u;?Cba{$x^>cn=%E5O79%`Q|HKNcw`?7w=xA``0 zS}rtK%2WMJy%lKD3jm*S7XYf?#^LjLCt{X@g9Dh_!2E3-efM$c*9l&U&tNx{dp#`< zbeAOD^B9J^B2!^5DO7GN_d0etT&tqA4A0rL;I4%yI~*=@_hI?95WL;tGVK&_!XwX} z4iX`Zvh>uzqa6+|5tqYh!L=PijSkCg0bVr!ch&tHQ}^S#ufsp|hax}`pa@U|C;}7# ziU37`B0v$K2v7tl0u+H~2m-aPU5>Pm`~+Q=V8B`9+U3>X`qlaWyP3NGtovl$yPqKi zq-s(GC;}7#iU37`B0v$K2v7tl0u%v?07c;U0)hH6Ptc3cFcMPDcDTf%#}~X2o}()h z)Z0Jcygyuw0@oA-UD^xS{LqGP3qVwXaMOeno&Vp#JjT@hVciY2|EJ~)HA~e$tBzHD zqbgffS9!W}yZ15g9iA_G#@*j6<6ZyZnst8BdD8Juj)bF?`6?vanxDbUf}2^c@zw+f z6L3=?C-ZEAUxiaRT#APyJ+X8=!KOqu#wQk2OKh8uO^7Krk&eZ5NDLm&!x`Sq#4ULN z55>x*Wwt*Pbu*Ke%n(V1ole~CR)6fuWqtyKw&chtkbuwiP2cx3= z8k>f5VL&(3HxcTc3IU~2f9NPXH`sqPY>FBtk>(JokyW|fbK1M6eKz9PGjkp$q--OQ znDQjzMQL$E>1rHX;pCOAY{iucdze9GmnJ;Mt)r8Cnx&+Zux}2LM5V0QlW11pjmUEV z(p2amFh7lq65RVk1HIEjQ>@Rf20)Ex7gGZ@u9DR-B5ziWhwsmv@-U~Ke*w^lbIUxd zLT40$OGH_%ATzx~(;?Ph>GgVvVNCTN7z(lE#3Gy}OyRv!;1L?w-6&cEFjncnj74?b`d{0WX0(4y9 zP)zch_91izMG)e(c#=8HQ+M$PmGSSwx*`O?;V;Fnk?j?G?nt zm1pA#h0IAev+q2#J{2Iy_ zxSQcoXFZL*Et_jZCWVH9N-!Otg5#Jr@FgTau#^Bb=&&rNr3h~= zg(=+z9p|~sQ6y07JfQeVDV;zAflUcCrP~Z{Q<+)7t@t1XIc&pzU|=R~JVe=*nL$I8 zcFi#G5CG1J=Zt^Egy{i{u2dt*A0rr}8;E2b>!{teCBhuLd(LR58wpnWAX;~u6{L9PLf!_~nk zu3$a}6qPmuQX}`M8YRcXuz8U;wuU8U#yrfux?z!AHg8qBc%$PlF#1-GWJbM=a=j9P zVsd&h0bkTy$!Oow*Z}@!tyQB2PDaP=4Al-P#btnvtNfJBo&I(gy9# zISsOTj>NVF@N{Ox&GfEYO^5XN$Y@aR7SmP5nI|wL_MBYn5g+m=c6( zCD#Y+=KjjIc}STdH#3ZSX=Ie7ks5|a8b=ADEonAr3h4VkbIsOF<7uuG0g3=cfFeK< zpa@U|C;}7#iU37`B0v$K2wWWqsQiEb&eZ*;?&ozs0v5pktovHszh51?Q7tF}6ak6= zMSvne5ugZA1SkR&0g3=cfFeKo7vrczzr5&|2mNxRU*+hR3;lA^@Bby>NTW~$ zC;}7#iU37`B0v$K2v7tl0u%v?07c*#jsQCUPxt>n!z+=hPZ6L9Py{Ff6ak6=MSvne z5ugZA1SkR&fejF#-~TrNh`y!>Py{Ff6ak6=MSvne5ugZA1SkR&0gAxy00QXy|830c zn7Y-vowcv1?W%dA=AP=0RgYCYUNu$utI7wx-|^0P{>QWEe$xGj+gI^N75A2Zu8eiP z(e*6n`<*v9<{h^{;w}7H&z2zqb41mNI4onryu?ZCVW8TK^NAGjXU3vHT@T!d7VgKK zXjbAD`QOtp!ufnmd(yPA2x&79HYC&WZc$vT)Nqq`M0CppuWISh4SeQ*F%(}#`@PEJj-b9T~6VyNuOIn*dM%G8ai{41MT%|7&08Uw#U z4vu1Bxx_52txR(Qq+y`Q7PAR2!Sx(Dk@UP9;pUN+bw1S+;bM_=3|*$sn4be-3(v^} z$I~xhFp8}e0KJd$3tT#u%73YA;H$Ky%Z+C4hnBI^a9+4(h>c3=7_U>9tKNms&EhwU zl9*1yjR$LNYCQ=}L}cfMMIm8FjeaC^&drEO%7U=QBEoSF=zVqeTEwZW*0@C^*2Tum zm7STh9!6AxkRN6&&dpEvIOgog9Vs?m(j3d`9Cb~LA#2BBeKNBSxYiZM70Qb)Q@KPu z`!Z{!%P9d*)p^n0(u)+N_D;{PUQ;?&Ym3SGN1g`vnTR+r$elQa@mrgB(Q*c27yfmXJM)EL6 zlycAsv0mdG)UGWTBYSiT;Ii@H?r7!=RK7d6@?#=+Iw(+zk0;S!!qk?D0c>3RCo)OM zx&~z}X!loH@@*B8VQ2+lu=>?e&Fs4Q7HX>WgBs|QhUZ2Db;+rVM6#>~k^bSDR zjxx8@D0*P6OlzWz?K_!?yO|^Bh$RupdP^bn=_SZDC)vP*C`=~iVTSd~PV(zQ#R^R} z3XY30&PF@~eADQ+c@9~$ZBH*}mVq`NU9Ci}IYA{QrC;Gf_hLd=bzb!G(Qt+o>p zaC?gFRUFzl#$rqc1HL}=GG7Ex`b{j;nI$)~So~@Z)MF)J9Ek5`D{(>rS79uA7+$|& zz)%=rNNUd}V@oLwsn&*L$!r6d>G%IFT)d!JQv@gi6ak6=MSvne5ugZA1SkR&0g3=c z;3`3Y&i}8HilAyx1SkR&0g3=cfFeKG2v7tl z0u%v?07ZZzKoOt_Py{Ff6oIP*0m}b>l~e>(gCal?pa@U|C;}7#iU37`B0v$K2v7tl z0$Ydxn*Z-$5=`CAwV|4SuIaD-yXuar=Ty~Jro0b(y`Hq^2KRd^K3H+F;*RonmOrcP zU1d|QZ@H$NRgUK~zkp<${87rYI}tI_HXwCS=~M`(93m!*h?2wug%;w+QRy()7Ir0& zPh658QRU1qJvUKj*pg-_+p@PI{)Jr|DI$w$DZ(TEjuMnHBHJotdTp!J{n=YR%q-?k zDGE+$%3z9((GkRErsIlvTnZ71TnuGz@i3j)P6HEDQllnpdEHDgK}aR0D`BTXN2f6R z+Xzq+Av!a{9J2ven+d1 zrPDISvXZ6b8Gs$@n^KXjN3=%BT_Bk%fTldBPcMjYRr#7)+UAHI`}G+7kJGLxzg^*UI$DY=OS!W zkP(Mul!d&2p;MG1z_LmL(1dNwK{=D%4u12ZK~c$CRg3Z|PKXhjKxkY99$PPGV(HW`q+=0p!YFlk+`Q~gS z(8Pq*1ZtZyUZW0AQ>1y0#IXhRY_`tLEF&9|)FNa0G?bQwUW*;D(@%BP&HNLCUjj8n4|{>J~A07=Acb9oAAxgz!qxdhHMQYgVfj& z^9y1SVClkbSSs~*R*#;q1sO#kUc~;;K=1U>6x-Nn=jT_l)ozB58%AJQQm$pn5fQji zZ7sk^lw+M?oXECC?8#Pnn6QGnXyn(8ELhSKRC&aPEbSt{rj693S*U_FZR{EeX&NxR zLFF>ls(lSQvz3Up@z#8Gg(&cH3kw2ppki=}HeMsLold5=_F9x>y+{kxD;q^-jbui* zVeYr)jJQ_AF%oLyLE@t~uo}gg^&pb+omM@7L{bvw(?BwW2lw>d4=vThG^ zuX4RriDjsSP0B|}HYZvc&sKPt)r)n+9B{?wakZ^bW#_m5ujoclRM{?$;gtBE9S_r^iGDR z2unSlMDAyGcD_~^h*f$ACPs&iXO&)+eK9ASRw4(wU2_Hy>>cUPRjO45+7eTJWYhD| z{r{U>!l2P90u%v?07ZZzKoOt_Py{Ff6ak6=MSvpk3`C%^_Ce;A?muGcURal@OV=&d z9jiN1x4&+CU3u*%YCll>7qxGwdUw^EtDaw#teUTysv4+bD!)vPspfdi5ZomA%j*BG{!;ayR{ufu+3HyJjaC0s z^=Z#rJ+JY+(3AD7dQN*zdB!|_o(@lw=N3<$$Lao=``hj>xpoidw{>r?du`p`x?Q!usQs_nFV;R*`<$v*Ry|PFRdrX@EwFFl z$CYoZd`;z}l^4D5_rBBnC*GHM*Sx2_C%q%y`)ZvvKdt#z&F5-fRO72@sJX7DqWY87 ze_#E0^-HUtT|HKPpt`c^`&A#ROjRya9<4lFd3R-NC0luIWm&CM8?BwGJp}tB{;uX7 zHLt75R<~6@tLiJy!0M(dQv@gi6ak9B)rdf~YloAIMv}?13FejNz$?swmzx7GGY4L3 z4m@TKyu=)Mu{rP}bKr&Mz@z5CBj&&h%z-~L2cB;Z{GmDUJagc==D;7A1J5xB9ySLa zG6%BTT}{sU1z$Sl7dSq+)U_C8GB(dH+B{Qio;_IO+UXdOXQJtj_0HvCOK8XvI${YO zMxn@&QT}vaZ(@1S5;|lF9khf7ETIqz>C*cx@A@pE1C~&)C3GJO>ABo%3Eg7}?YD&P zMxpe;%<6%`@W}E$OQ^>Z>b8WsETK+IsKXL!w}jd(p&$zB2nH-6za`|eg!ZD4UfNbm zsKpX$wuJVekS^~o%eyAayWN&hqb2k#OXyA%(sjAR61v?IYOsWMp^&Z%Yk9ZR^6oav zyIU=xTP!gzSl&H=LVDevx5T{P67!tp-C4`KbrjNbS+j&zE$LP)p|mBGLLnU`*%Fd0 zp);0rNy|IY@-ATs#Vw(jCEc>g$q^iWrKhb43~3h64{WC`793DsLdH=vN7=k=D*b(YX}OS)?

Developer Resources

  • ANMS REST API Docs
  • Grafana - This powers the 'Monitor' tab. Tip: Press the 'Esc' key in the Monitor tab after clicking within the display to exit Kiosk mode and access the full UI interface.
  • -
  • Adminer (DB Management) (if ANMS is configured with 'dev' profile
  • +
  • Adminer (DB Management) (if ANMS is configured with 'dev' profile
    diff --git a/grafana/provisioning/datasources/datasource.yml b/grafana/provisioning/datasources/datasource.yml index 5992fb02..6a12c441 100644 --- a/grafana/provisioning/datasources/datasource.yml +++ b/grafana/provisioning/datasources/datasource.yml @@ -31,6 +31,7 @@ datasources: jsonData: keepCookies: [] sslmode: disable + database: amp_core user: grafana secureJsonData: password: grafana From ca174dd9f5d011be9e554a47e02da17520421181 Mon Sep 17 00:00:00 2001 From: David Edell Date: Mon, 1 Dec 2025 11:57:10 -0500 Subject: [PATCH 04/51] Added upgrading file --- UPGRADING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 UPGRADING.md diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..d19f05fa --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,17 @@ +This file will contain upgrading instructions for all future tagged releases. + +# Upgrading v2.0.0 to main + +## Grafana DB Update +A new database named `grafana_internal_db` needs to be created in postgres. + +This can be done from the commandline or via the dev UI. + +In the latter case, ensure your instance is started with the `dev` and `full` profiles. Go to Adminer (link provided in UI Help page) and either manually create the DB or 'Execute SQL' and upload the file `grafana/create_grafana_db.sql` + +# Upgrading v1.x to v.2.0.0 + +It is recommended to start fresh (delete any existing ANMS-related containers and volumes) when transitioning from ANMS v1 to v2. + +If you have data or customizations in a v1 installation that you need to migrate, please contact us or open an issue to discuss. + From d968c1a6d348deffd3b6a22f634fcb676723da9e Mon Sep 17 00:00:00 2001 From: David Edell Date: Tue, 2 Dec 2025 10:32:24 -0500 Subject: [PATCH 05/51] Add amp-manager to UI Status page --- anms-ui/config_ui_env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anms-ui/config_ui_env.js b/anms-ui/config_ui_env.js index 4acba843..7fef23ba 100644 --- a/anms-ui/config_ui_env.js +++ b/anms-ui/config_ui_env.js @@ -24,7 +24,7 @@ window.anms_env_config = { VUE_APP_STATUS_REFRESH_RATE: 60000, SERVICE_INFO: { names: [ - "adminer","anms-core","authnz", + "adminer","anms-core","authnz","amp-manager", "aricodec","postgres", "redis","mqtt-broker","transcoder", "grafana","grafana-image-renderer" From c24e9a3d06f97075d36b8719c3ebcdda93c2ac91 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 9 Dec 2025 13:52:05 -0500 Subject: [PATCH 06/51] added inifinity-datasource plugin and example panels --- docker-compose.yml | 2 + .../provisioning/dashboards/anms-monitor.json | 227 ++++++++++++++++++ .../provisioning/dashboards/dashboards.yaml | 3 + .../provisioning/datasources/datasource.yml | 14 ++ 4 files changed, 246 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 394e1db0..317c92bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -229,6 +229,8 @@ services: GF_DATABASE_PASSWORD: ${DB_PASSWORD} GF_DATABASE_SSL_MODE: disable GF_DATABASE_PATH: "" # Explicitly disable internal sqlite for clarity + GF_PLUGINS_PREINSTALL: "yesoreyeram-infinity-datasource" + GODEBUG: x509negativeserial=1 #fix for tls: failed to parse certificate from server: x509: negative serial number grafana-image-renderer: hostname: grafana-image-renderer diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json index 0439faeb..2b45e14c 100644 --- a/grafana/provisioning/dashboards/anms-monitor.json +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -13,6 +13,12 @@ "name": "PostgreSQL", "version": "1.0.0" }, + { + "type": "datasource", + "id": "Infinity", + "name": "Infinity", + "version": "1.0.0" + }, { "type": "panel", "id": "table", @@ -597,7 +603,228 @@ ], "title": "ARIs", "type": "table" + }, + { + "id": 73, + "type": "table", + "title": "Hello_rpt_agent_1", + "gridPos": { + "x": 0, + "y": 16, + "h": 8, + "w": 12 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "footer": { + "reducers": [] + }, + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "hideFrom": { + "viz": false + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "filters": [], + "format": "table", + "global_query_id": "", + "parser": "backend", + "refId": "A", + "root_selector": "", + "source": "url", + "type": "uql", + "uql": " parse-json\n | scope \"ari://ietf/dtnma-agent/CONST/hello\"\n | extend \"hello\"=array_to_map(\"ari://ietf/dtnma-agent/CONST/hello\",'sw_vendor','sw_version', 'capabilities')\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"sw_vendor\"=\"hello.sw_vendor\", \"sw_version\"=\"hello.sw_version\", \"capabilities\"=\"hello.capabilities\"\n | order by \"time\" desc\n", + "url": "http://anms-core:5555/report/entries/table/1/b%27%5Cxf6%27", + "url_options": { + "data": "", + "headers": [], + "method": "GET", + "params": [ + { + "key": "agent_id", + "value": "1" + }, + { + "key": "nonce_cbor", + "value": "b%27%5Cxf6%27" + } + ] + } + } + ], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "sortBy": [] + } + }, + { + "id": 74, + "type": "timeseries", + "title": "ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx)", + "gridPos": { + "x": 0, + "y": 8, + "h": 8, + "w": 12 + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "barAlignment": 0, + "barWidthFactor": 0.6, + "lineWidth": 1, + "fillOpacity": 0, + "gradientMode": "none", + "spanNulls": false, + "insertNulls": false, + "showPoints": "auto", + "showValues": false, + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "axisBorderShow": false, + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "filters": [], + "format": "table", + "global_query_id": "", + "parser": "backend", + "refId": "A", + "root_selector": "", + "source": "url", + "type": "uql", + "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_3_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_3_6\"\n ", + "url": "1/b%27%5Cx18%7B%27", + "url_options": { + "data": "", + "method": "GET" + } + }, + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "filters": [], + "format": "table", + "global_query_id": "", + "hide": false, + "parser": "backend", + "refId": "B", + "root_selector": "", + "source": "url", + "type": "uql", + "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_2_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_2_6\"\n ", + "url": "2/b%27%5Cx18%7B%27", + "url_options": { + "data": "", + "method": "GET" + } + } + ], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "options": { + "tooltip": { + "mode": "single", + "sort": "none", + "hideZeros": false + }, + "legend": { + "showLegend": true, + "displayMode": "table", + "placement": "right", + "calcs": [] } + } + } + ], "refresh": false, "schemaVersion": 37, diff --git a/grafana/provisioning/dashboards/dashboards.yaml b/grafana/provisioning/dashboards/dashboards.yaml index a9f62129..42423135 100644 --- a/grafana/provisioning/dashboards/dashboards.yaml +++ b/grafana/provisioning/dashboards/dashboards.yaml @@ -11,3 +11,6 @@ providers: # The path option specifies the directory where your JSON files are located # In this case, it can be the current directory ('.') path: /etc/grafana/provisioning/dashboards + + + diff --git a/grafana/provisioning/datasources/datasource.yml b/grafana/provisioning/datasources/datasource.yml index 6a12c441..ffaf3482 100644 --- a/grafana/provisioning/datasources/datasource.yml +++ b/grafana/provisioning/datasources/datasource.yml @@ -39,3 +39,17 @@ datasources: version: 1 editable: true isDefault: true + +- name: report_entries_table_endpoint + type: yesoreyeram-infinity-datasource + typeName: Infinity + access: proxy + url: http://anms-core:5555/report/entries/table/ + user: + database: + basicAuth: false + isDefault: false + jsonData: + global_queries: [] + pdcInjected: false + From ddc87987857e86468e6d193b85f16134d17384ce Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 9 Dec 2025 14:24:38 -0500 Subject: [PATCH 07/51] updated layout of panels --- .../provisioning/dashboards/anms-monitor.json | 516 ++++++++---------- 1 file changed, 241 insertions(+), 275 deletions(-) diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json index 2b45e14c..e3a1433d 100644 --- a/grafana/provisioning/dashboards/anms-monitor.json +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -1,37 +1,4 @@ { - "__elements": {}, - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "9.1.3" - }, - { - "type": "datasource", - "id": "postgres", - "name": "PostgreSQL", - "version": "1.0.0" - }, - { - "type": "datasource", - "id": "Infinity", - "name": "Infinity", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "" - }, - { - "type": "panel", - "id": "timeseries", - "name": "Time series", - "version": "" - } - ], "annotations": { "list": [ { @@ -57,9 +24,8 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": null, + "id": 1, "links": [], - "liveNow": false, "panels": [ { "datasource": { @@ -73,11 +39,13 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, + "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "opacity", @@ -86,6 +54,7 @@ "tooltip": false, "viz": false }, + "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 1, "pointSize": 5, @@ -93,6 +62,7 @@ "type": "linear" }, "showPoints": "always", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -108,7 +78,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -135,11 +105,12 @@ "showLegend": true }, "tooltip": { + "hideZeros": false, "mode": "multi", "sort": "none" } }, - "pluginVersion": "8.2.4", + "pluginVersion": "12.3.0", "targets": [ { "datasource": { @@ -188,7 +159,12 @@ }, "custom": { "align": "auto", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, "inspect": false }, "mappings": [], @@ -197,7 +173,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -216,16 +192,10 @@ }, "id": 2, "options": { - "footer": { - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, + "cellHeight": "sm", "showHeader": true }, - "pluginVersion": "9.1.3", + "pluginVersion": "12.3.0", "targets": [ { "datasource": { @@ -274,7 +244,12 @@ }, "custom": { "align": "left", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, "inspect": false, "minWidth": 100 }, @@ -284,7 +259,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -472,17 +447,11 @@ }, "id": 13, "options": { - "footer": { - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, + "cellHeight": "sm", "showHeader": true, "sortBy": [] }, - "pluginVersion": "9.1.3", + "pluginVersion": "12.3.0", "targets": [ { "datasource": { @@ -531,7 +500,12 @@ }, "custom": { "align": "auto", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, "inspect": false }, "mappings": [], @@ -540,7 +514,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -552,23 +526,17 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 12, + "h": 9, + "w": 24, "x": 0, "y": 16 }, "id": 6, "options": { - "footer": { - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, + "cellHeight": "sm", "showHeader": true }, - "pluginVersion": "9.1.3", + "pluginVersion": "12.3.0", "targets": [ { "datasource": { @@ -605,230 +573,229 @@ "type": "table" }, { - "id": 73, - "type": "table", - "title": "Hello_rpt_agent_1", - "gridPos": { - "x": 0, - "y": 16, - "h": 8, - "w": 12 - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "footer": { - "reducers": [] - }, - "cellOptions": { - "type": "auto" - }, - "inspect": false, - "hideFrom": { - "viz": false - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - }, - "pluginVersion": "12.3.0", - "targets": [ - { - "columns": [], "datasource": { "type": "yesoreyeram-infinity-datasource", "uid": "PA0935669E4DE7FB6" }, - "filters": [], - "format": "table", - "global_query_id": "", - "parser": "backend", - "refId": "A", - "root_selector": "", - "source": "url", - "type": "uql", - "uql": " parse-json\n | scope \"ari://ietf/dtnma-agent/CONST/hello\"\n | extend \"hello\"=array_to_map(\"ari://ietf/dtnma-agent/CONST/hello\",'sw_vendor','sw_version', 'capabilities')\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"sw_vendor\"=\"hello.sw_vendor\", \"sw_version\"=\"hello.sw_version\", \"capabilities\"=\"hello.capabilities\"\n | order by \"time\" desc\n", - "url": "http://anms-core:5555/report/entries/table/1/b%27%5Cxf6%27", - "url_options": { - "data": "", - "headers": [], - "method": "GET", - "params": [ - { - "key": "agent_id", - "value": "1" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - { - "key": "nonce_cbor", - "value": "b%27%5Cxf6%27" + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] } - ] - } - } - ], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" - }, - "options": { - "showHeader": true, - "cellHeight": "sm", - "sortBy": [] - } - }, - { - "id": 74, - "type": "timeseries", - "title": "ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx)", - "gridPos": { - "x": 0, - "y": 8, - "h": 8, - "w": 12 - }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "line", - "lineInterpolation": "smooth", - "barAlignment": 0, - "barWidthFactor": 0.6, - "lineWidth": 1, - "fillOpacity": 0, - "gradientMode": "none", - "spanNulls": false, - "insertNulls": false, - "showPoints": "auto", - "showValues": false, - "pointSize": 5, - "stacking": { - "mode": "none", - "group": "A" - }, - "axisPlacement": "auto", - "axisLabel": "", - "axisColorMode": "text", - "axisBorderShow": false, - "scaleDistribution": { - "type": "linear" }, - "axisCenteredZero": false, - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 74, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" } }, - "color": { - "mode": "palette-classic" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "12.3.0", + "targets": [ + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" }, - { - "color": "red", - "value": 80 + "filters": [], + "format": "table", + "global_query_id": "", + "parser": "backend", + "refId": "A", + "root_selector": "", + "source": "url", + "type": "uql", + "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_3_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_3_6\"\n ", + "url": "1/b%27%5Cx18%7B%27", + "url_options": { + "data": "", + "method": "GET" } - ] - } + }, + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "filters": [], + "format": "table", + "global_query_id": "", + "hide": false, + "parser": "backend", + "refId": "B", + "root_selector": "", + "source": "url", + "type": "uql", + "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_2_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_2_6\"\n ", + "url": "2/b%27%5Cx18%7B%27", + "url_options": { + "data": "", + "method": "GET" + } + } + ], + "title": "ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx)", + "type": "timeseries" }, - "overrides": [] - }, - "pluginVersion": "12.3.0", - "targets": [ { - "columns": [], "datasource": { "type": "yesoreyeram-infinity-datasource", "uid": "PA0935669E4DE7FB6" }, - "filters": [], - "format": "table", - "global_query_id": "", - "parser": "backend", - "refId": "A", - "root_selector": "", - "source": "url", - "type": "uql", - "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_3_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_3_6\"\n ", - "url": "1/b%27%5Cx18%7B%27", - "url_options": { - "data": "", - "method": "GET" - } - }, - { - "columns": [], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "hideFrom": { + "viz": false + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] }, - "filters": [], - "format": "table", - "global_query_id": "", - "hide": false, - "parser": "backend", - "refId": "B", - "root_selector": "", - "source": "url", - "type": "uql", - "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_2_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_2_6\"\n ", - "url": "2/b%27%5Cx18%7B%27", - "url_options": { - "data": "", - "method": "GET" - } - } - ], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" - }, - "options": { - "tooltip": { - "mode": "single", - "sort": "none", - "hideZeros": false - }, - "legend": { - "showLegend": true, - "displayMode": "table", - "placement": "right", - "calcs": [] + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 73, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.3.0", + "targets": [ + { + "columns": [], + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "PA0935669E4DE7FB6" + }, + "filters": [], + "format": "table", + "global_query_id": "", + "parser": "backend", + "refId": "A", + "root_selector": "", + "source": "url", + "type": "uql", + "uql": " parse-json\n | scope \"ari://ietf/dtnma-agent/CONST/hello\"\n | extend \"hello\"=array_to_map(\"ari://ietf/dtnma-agent/CONST/hello\",'sw_vendor','sw_version', 'capabilities')\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"sw_vendor\"=\"hello.sw_vendor\", \"sw_version\"=\"hello.sw_version\", \"capabilities\"=\"hello.capabilities\"\n | order by \"time\" desc\n", + "url": "http://anms-core:5555/report/entries/table/1/b%27%5Cxf6%27", + "url_options": { + "data": "", + "headers": [], + "method": "GET", + "params": [ + { + "key": "agent_id", + "value": "1" + }, + { + "key": "nonce_cbor", + "value": "b%27%5Cxf6%27" + } + ] + } + } + ], + "title": "Hello_rpt_agent_1", + "type": "table" } - } - } - ], - "refresh": false, - "schemaVersion": 37, - "style": "dark", + "preload": false, + "refresh": "", + "schemaVersion": 42, "tags": [], "templating": { "list": [] @@ -841,6 +808,5 @@ "timezone": "", "title": "Monitor Page", "uid": "mwvijjmvk", - "version": 2, - "weekStart": "" -} + "version": 5 +} \ No newline at end of file From 04d219c8318e0867569761a71594e81f7b9bdd0c Mon Sep 17 00:00:00 2001 From: David Edell Date: Mon, 8 Dec 2025 23:22:15 -0500 Subject: [PATCH 08/51] Fix core '/services' REST API to return JSON instead of text --- anms-core/anms/routes/system_status.py | 3 ++- anms-ui/public/app/store/modules/service_status.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/anms-core/anms/routes/system_status.py b/anms-core/anms/routes/system_status.py index 2ab158e3..3db24258 100644 --- a/anms-core/anms/routes/system_status.py +++ b/anms-core/anms/routes/system_status.py @@ -138,4 +138,5 @@ async def sys_status_get_version(): async def sys_status_get_services_status(): statuses = get_containers_status() logger.debug(f"Checking all services' status: {str(statuses)}") - return json.dumps(statuses) + return statuses + diff --git a/anms-ui/public/app/store/modules/service_status.js b/anms-ui/public/app/store/modules/service_status.js index dd287d42..9b88b93f 100644 --- a/anms-ui/public/app/store/modules/service_status.js +++ b/anms-ui/public/app/store/modules/service_status.js @@ -72,8 +72,8 @@ export default { api.methods.apiGetServiceStatus().then(res => { console.log("updateStatus response", res.data); let jsonStatus = {}; - try{ - jsonStatus = JSON.parse(res.data); //?Asomehow axios does not parse the Json response + try { + jsonStatus = (typeof jsonStatus === 'object') ? res.data : JSON.parse(res.data); } catch (e){ console.error(e) jsonStatus = {}; From fc9caae19e78505c2f1896509c7f07b53b9494fd Mon Sep 17 00:00:00 2001 From: David Edell Date: Mon, 8 Dec 2025 23:26:41 -0500 Subject: [PATCH 09/51] Core REST API bugfixes to detect timeouts and resolve potential error with overloaded status variable. --- anms-core/anms/routes/network_manager.py | 12 +++++++++++- anms-core/anms/routes/transcoder.py | 23 +++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/anms-core/anms/routes/network_manager.py b/anms-core/anms/routes/network_manager.py index 233656fd..d7735131 100644 --- a/anms-core/anms/routes/network_manager.py +++ b/anms-core/anms/routes/network_manager.py @@ -91,7 +91,17 @@ def do_nm_put_hex_eid(eid: str, ari: str): logger.info('post to nm manager %s with eid %s and data %s' % (url, eid, ari)) try: - request = requests.post(url=url, data=ari, headers={'Content-Type': 'text/plain'}) + request = requests.post(url=url, + data=ari, + headers={'Content-Type': 'text/plain'}, + timeout=(2.0, 8.0) # 2s for manager to connect, 8s for it to respond + ) + except requests.exceptions.ConnectTimeout: + return status.HTTP_504_GATEWAY_TIMEOUT + except requests.exceptions.ReadTimeout: + return status.HTTP_504_GATEWAY_TIMEOUT + except requests.exceptions.Timeout: + return status.HTTP_504_GATEWAY_TIMEOUT except Exception: return status.HTTP_500_INTERNAL_SERVER_ERROR return request.status_code diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index f218dd6b..c43a6a2a 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -85,16 +85,16 @@ async def transcoder_put_input_cbor(input_cbor: str): session.refresh(c1) transcoder_log_id = c1.transcoder_log_id session.commit() - status = "Submitted ARI to transcoder" + state = "Submitted ARI to transcoder" else: # the input_ari has already been submitted - status = "ARI previously submitted, check log" + state = "ARI previously submitted, check log" transcoder_log_id = curr_uri.transcoder_log_id logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) - return {"id": transcoder_log_id, "status": status} + return {"id": transcoder_log_id, "status": state} @router.get("/ui/incoming/await/{cbor}/hex", status_code=status.HTTP_200_OK) @@ -200,16 +200,16 @@ def transcoder_put_str(input_ari: str): session.refresh(c1) transcoder_log_id = c1.transcoder_log_id session.commit() - status = "Submitted ARI to transcoder" + state = "Submitted ARI to transcoder" else: # the input_ari has already been submitted - status = "ARI previously submitted, check log" + state = "ARI previously submitted, check log" transcoder_log_id = curr_uri.transcoder_log_id logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) - return {"id": transcoder_log_id, "status": status} + return {"id": transcoder_log_id, "status": state} @@ -223,21 +223,24 @@ async def transcoder_send_ari_str(eid: str, ari: str): # Retrieve details and wait for completion retries = 10 while True: + # Wait for request to process before checking state + await asyncio.sleep(1) + info = do_transcoder_log_by_id(idinfo["id"]) + if info.parsed_as != "pending": break if retries <= 0: return { "idinfo" : idinfo, "info" : info, "status" : 504 } - await asyncio.sleep(1) retries -= 1 if info.parsed_as == "ERROR": return { "idinfo" : idinfo, "info" : info, "status" : 500 } - + # Publish - status = do_nm_put_hex_eid( eid, info.cbor ) + state = do_nm_put_hex_eid( eid, info.cbor ) - return { "idinfo" : idinfo, "info" : info, "status" : status } + return { "idinfo" : idinfo, "info" : info, "status" : state } except Exception as e: logger.exception(e) return status.HTTP_500_INTERNAL_SERVER_ERROR From 356a03407010a96545f38423db8aab0c2c2d4ec5 Mon Sep 17 00:00:00 2001 From: David Linko Date: Wed, 17 Dec 2025 17:42:10 -0500 Subject: [PATCH 10/51] initial dev creating a internal transcoder --- anms-core/anms/config_template.yaml | 4 +- anms-core/anms/routes/transcoder.py | 107 +++--------- anms-core/anms/shared/config.py | 4 + anms-core/anms/shared/transmogrifier.py | 218 ++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 80 deletions(-) create mode 100644 anms-core/anms/shared/transmogrifier.py diff --git a/anms-core/anms/config_template.yaml b/anms-core/anms/config_template.yaml index a4d3422e..867b5074 100644 --- a/anms-core/anms/config_template.yaml +++ b/anms-core/anms/config_template.yaml @@ -108,4 +108,6 @@ EMAIL_ENABLED: EMAIL_ENABLED_TEMPLATE EMAIL_FROM_NAME: EMAIL_FROM_NAME_TEMPLATE EMAIL_FROM_EMAIL: EMAIL_FROM_EMAIL_TEMPLATE EMAIL_TEMPLATES_DIR: EMAIL_TEMPLATES_DIR_TEMPLATE -EMAIL_RESET_TOKEN_EXPIRE_LIFETIME: EMAIL_RESET_TOKEN_EXPIRE_LIFETIME_TEMPLATE \ No newline at end of file +EMAIL_RESET_TOKEN_EXPIRE_LIFETIME: EMAIL_RESET_TOKEN_EXPIRE_LIFETIME_TEMPLATE + +Transcoder: TRANSCODER_TEMPLATE \ No newline at end of file diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index f218dd6b..8cb7ba41 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -19,7 +19,6 @@ # the prime contract 80NM0018D0004 between the Caltech and NASA under # subcontract 1658085. # -import json import time import asyncio @@ -28,15 +27,15 @@ from fastapi_pagination import Page, Params from fastapi_pagination.ext.async_sqlalchemy import paginate -from sqlalchemy import select, or_, String, desc +from sqlalchemy import select, or_, desc from anms.models.relational import get_session from anms.components.schemas import TranscoderLog as TL from anms.models.relational import get_async_session from anms.models.relational.transcoder_log import TranscoderLog -from anms.shared.mqtt_client import MQTT_CLIENT -from anms.shared.opensearch_logger import OpenSearchLogger +from anms.shared.opensearch_logger import OpenSearchLogger +from anms.shared.transmogrifier import TRANSMORGIFIER from anms.routes.network_manager import do_nm_put_hex_eid router = APIRouter(tags=["Transcoder"]) @@ -65,64 +64,22 @@ async def paged_transcoder_log(query: str, params: Params = Depends()): @router.get("/db/id/{id}", status_code=status.HTTP_200_OK, response_model=TL) def transcoder_log_by_id(id: str): - return do_transcoder_log_by_id(id) + return _do_transcoder_log_by_id(id) -def do_transcoder_log_by_id(id: str): +def _do_transcoder_log_by_id(id: str): with get_session() as session: return TranscoderLog.query.filter_by(transcoder_log_id=id).first() # PUT /ui/incoming/{cbor}/hex @router.put("/ui/incoming/{input_cbor}/hex", status_code=status.HTTP_200_OK) async def transcoder_put_input_cbor(input_cbor: str): - msg = json.dumps({'uri': input_cbor}) - transcoder_log_id = None - with get_session() as session: - curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==input_cbor, TranscoderLog.cbor==input_cbor)).first() - if curr_uri is None: - c1 = TranscoderLog(input_string=input_cbor, parsed_as='pending') - session.add(c1) - session.flush() - session.refresh(c1) - transcoder_log_id = c1.transcoder_log_id - session.commit() - status = "Submitted ARI to transcoder" - else: - # the input_ari has already been submitted - status = "ARI previously submitted, check log" - transcoder_log_id = curr_uri.transcoder_log_id - - logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) - MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) - - return {"id": transcoder_log_id, "status": status} + return _transcoder_put_cbor(input_cbor) @router.get("/ui/incoming/await/{cbor}/hex", status_code=status.HTTP_200_OK) async def transcoder_put_cbor_await(cbor: str): - curr_uri = "" - msg = json.dumps({'uri': cbor}) - transcoder_log_id = None - with get_session() as session: - curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==cbor, TranscoderLog.cbor==cbor)).first() - if curr_uri is None: - c1 = TranscoderLog(input_string=cbor, parsed_as='pending') - session.add(c1) - session.flush() - session.refresh(c1) - transcoder_log_id = c1.transcoder_log_id - session.commit() - logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) - MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) - else: - transcoder_log_id = curr_uri.transcoder_log_id - if curr_uri.parsed_as != "pending": - if curr_uri.parsed_as == "ERROR": - curr_uri = "ARI://BADARI" - else: - curr_uri = curr_uri.uri - return {"data": curr_uri} - - + curr_entry = _transcoder_put_cbor(cbor) + transcoder_log_id = curr_entry["id"] while True: with get_session() as session: curr_uri = TranscoderLog.query.filter(TranscoderLog.transcoder_log_id==transcoder_log_id).first() @@ -137,36 +94,32 @@ async def transcoder_put_cbor_await(cbor: str): return {"data": curr_uri} -# PUT /ui/incoming/str Body is str ARI to send to transcoder -@router.get("/ui/incoming/await/str", status_code=status.HTTP_200_OK) -async def transcoder_put_await_str(input_ari: str): - input_ari = input_ari.strip() - msg = json.dumps({"uri": input_ari}) +def _transcoder_put_cbor(input_cbor): transcoder_log_id = None - curr_uri = None with get_session() as session: - curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==input_ari,TranscoderLog.ari==input_ari, TranscoderLog.cbor==input_ari)).first() - + curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==input_cbor, TranscoderLog.cbor==input_cbor)).first() if curr_uri is None: - c1 = TranscoderLog(input_string=input_ari, parsed_as='pending') + c1 = TranscoderLog(input_string=input_cbor, parsed_as='pending') session.add(c1) session.flush() session.refresh(c1) transcoder_log_id = c1.transcoder_log_id session.commit() - logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) - MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) + status = "Submitted ARI to transcoder" + TRANSMORGIFIER.transcode(input_cbor) else: + # the input_ari has already been submitted + status = "ARI previously submitted, check log" transcoder_log_id = curr_uri.transcoder_log_id - if curr_uri.parsed_as != "pending": - if curr_uri.parsed_as == "ERROR": - curr_uri = "ARI://BADARI" - else: - curr_uri = curr_uri.uri - return {"data": curr_uri} - - + return {"id": transcoder_log_id, "status": status} + + +# PUT /ui/incoming/str Body is str ARI to send to transcoder +@router.get("/ui/incoming/await/str", status_code=status.HTTP_200_OK) +async def transcoder_put_await_str(input_ari: str): + curr_entry = _transcoder_put_str(input_ari) + transcoder_log_id = curr_entry["id"] while(True): with get_session() as session: curr_uri = TranscoderLog.query.filter_by(transcoder_log_id=transcoder_log_id).first() @@ -178,18 +131,16 @@ async def transcoder_put_await_str(input_ari: str): break time.sleep(1) - return {"data": curr_uri} # PUT /ui/incoming/str Body is str ARI to send to transcoder @router.put("/ui/incoming/str", status_code=status.HTTP_200_OK) def transcoder_incoming_str(input_ari: str): - return transcoder_put_str(input_ari) + return _transcoder_put_str(input_ari) -def transcoder_put_str(input_ari: str): +def _transcoder_put_str(input_ari: str): input_ari = input_ari.strip() - msg = json.dumps({"uri": input_ari}) transcoder_log_id = None with get_session() as session: curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==input_ari,TranscoderLog.ari==input_ari, TranscoderLog.cbor==input_ari)).first() @@ -201,14 +152,12 @@ def transcoder_put_str(input_ari: str): transcoder_log_id = c1.transcoder_log_id session.commit() status = "Submitted ARI to transcoder" + TRANSMORGIFIER.transcode(input_ari) else: # the input_ari has already been submitted status = "ARI previously submitted, check log" transcoder_log_id = curr_uri.transcoder_log_id - logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) - MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) - return {"id": transcoder_log_id, "status": status} @@ -218,12 +167,12 @@ def transcoder_put_str(input_ari: str): async def transcoder_send_ari_str(eid: str, ari: str): try: # Perform translation (API wrapper) - idinfo = transcoder_put_str(ari) + idinfo = _transcoder_put_str(ari) # Retrieve details and wait for completion retries = 10 while True: - info = do_transcoder_log_by_id(idinfo["id"]) + info = _do_transcoder_log_by_id(idinfo["id"]) if info.parsed_as != "pending": break if retries <= 0: diff --git a/anms-core/anms/shared/config.py b/anms-core/anms/shared/config.py index 7cdac01b..5c82458a 100644 --- a/anms-core/anms/shared/config.py +++ b/anms-core/anms/shared/config.py @@ -195,6 +195,10 @@ class BaseConfig(AbstractConfig): UI_PORT = 9030 UI_API_BASE = "/api/" + + #Transcoding + Transcoder = "Internal" + def on_finalized(self): pass diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py new file mode 100644 index 00000000..479c0d9b --- /dev/null +++ b/anms-core/anms/shared/transmogrifier.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2025 The Johns Hopkins University Applied Physics +# Laboratory LLC. + +# This file is part of the Asynchronous Network Management System (ANMS). + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This work was performed for the Jet Propulsion Laboratory, California +# Institute of Technology, sponsored by the United States Government under +# the prime contract 80NM0018D0004 between the Caltech and NASA under +# subcontract 1658085. + + +from anms.shared.config import ConfigBuilder +from anms.shared.logger import Logger +from anms.shared.mqtt_client import MQTT_CLIENT +from anms.shared.opensearch_logger import OpenSearchLogger +from anms.models.relational import get_session +from anms.models.relational.transcoder_log import TranscoderLog +import traceback +import ace +import io + +config = ConfigBuilder.get_config() +LOGGER = OpenSearchLogger(__name__, log_console=True) + + +# depending on what the config is for core will either use a MQTT server to send off commands or +# use an internal +class Transmorgifier: + + ''' The Transmogifier that can be configured to use an external or internal translator. ''' + def __init__(self, args): + # if the transcoding in internal to core + if config.Transcoder == "internal": + LOGGER.info('Connecting to SQL DB at %s', args.db_uri) + self._dbeng = sqlalchemy.create_engine(args.db_uri) + self._adms = ace.AdmSet(cache_dir=False) + self.transcode = _transcode_internal + self.reload = _reload_internal + self._adm_reload(None) + else: + # setting up the MQTT server instead + self.transcode = _transcode_mqtt + self.reload = _reload_mqtt + + def _transcode_mqtt(self, input): + msg = json.dumps({'uri': input}) + logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) + MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) + + def _transcode_internal(self, input): + # result object to fill in + res_obj = {} + res_obj['uri'] = "" + res_obj['cbor'] = "" + + try: + req_obj = json.loads(msg.payload) + LOGGER.info('Request %s', req_obj) + + in_text = req_obj['uri'].strip() + res_obj['inputString'] = in_text + in_lower = in_text.casefold() + if in_lower.startswith('ari:0x') or in_lower.startswith('0x'): + # Binary-to-text mode + res_obj['parsedAs'] = 'CBOR' + + if in_lower.startswith('ari:'): + in_text = in_text[4:] + + try: + in_bytes = ace.cborutil.from_hexstr(in_text) + dec = ace.ari_cbor.Decoder() + ari = dec.decode(io.BytesIO(in_bytes)) + LOGGER.debug('decoded as ARI %s', ari) + # ace.nickname.Converter(ace.nickname.Mode.FROM_NN, self._admsSession(self._dbeng), True)(ari) + ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, self._adms.db_session(), False)(ari) + + except Exception as err: + raise RuntimeError(f"Error decoding from `{in_text}`: {err}") from err + res_obj['cbor'] = in_text + res_obj['ari'] = f"{ari}" + + try: + enc = ace.ari_text.Encoder() + buf = io.StringIO() + enc.encode(ari, buf) + + out_text = buf.getvalue() + if not out_text.startswith('ari:'): + out_text = 'ari:' + out_text + LOGGER.debug('encoded as text %s', out_text) + except Exception as err: + raise RuntimeError(f"Error encoding from {ari}: {err}") from err + res_obj['uri'] = out_text + + else: + # Text-to-binary mode + res_obj['parsedAs'] = 'URI' + + try: + dec = ace.ari_text.Decoder() + ari = dec.decode(io.StringIO(in_text)) + LOGGER.debug('decoded as ARI %s', ari) + ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, self._adms.db_session(), False)(ari) + except Exception as err: + raise RuntimeError(f"Error decoding from `{in_text}`: {err}") from err + + # rencoding ari to ensure using non nicknames + try: + enc = ace.ari_text.Encoder() + buf = io.StringIO() + enc.encode(ari, buf) + + out_text = buf.getvalue() + if not out_text.startswith('ari:'): + out_text = 'ari:' + out_text + LOGGER.debug('encoded as text %s', out_text) + except Exception as err: + raise RuntimeError(f"Error encoding from {ari}: {err}") from err + + res_obj['uri'] = out_text + res_obj['ari'] = f"{ari}" + + try: + enc = ace.ari_cbor.Encoder() + buf = io.BytesIO() + enc.encode(ari, buf) + + hex_str = ace.cborutil.to_hexstr(buf.getvalue()) + LOGGER.info('encoded as binary %s', hex_str) + except Exception as err: + raise RuntimeError(f"Error encoding from {ari}: {err}") from err + res_obj['cbor'] = hex_str + + except Exception as err: + res_obj['ari'] = f'Failed to process: {err}' + res_obj['parsedAs'] = 'ERROR' + LOGGER.error('Failed to process: %s', err) + LOGGER.info('Traceback:\n%s', traceback.format_exc()) + + LOGGER.info('Response %s', res_obj) + + # client.publish('transcode/CodexFacing/Outgoing', json.dumps(res_obj)) + # just log it back into the database + + # store in transcoder database + with get_session() as session: + session.query(TranscoderLog).filter(TranscoderLog.input_string == res_obj['inputString']). \ + update({ + 'parsed_as': res_obj['parsedAs'], + 'ari': json.dumps(res_obj['ari']), + 'cbor': res_obj['cbor'], + 'uri': res_obj['uri'] + }) + session.commit() + + def _reload_mqtt(self,adm_name): + return + + def _reload_internal(self, adm_name): + try: + self._adm_reload(adm_name) + except Exception as err: + LOGGER.error('Failed to process reload: %s', err) + LOGGER.info('Traceback:\n%s', traceback.format_exc()) + + def _adm_reload(self, adm_name): + with self._dbeng.connect() as db_conn: + if adm_name: + LOGGER.info('Reloading one ADM: %s', adm_name) + curs = db_conn.execute('''\ +SELECT data_model.name, adm_data.updated_at, adm_data.data +FROM adm_data + INNER JOIN data_model ON adm_data.enumeration = data_model..data_model_id +WHERE adm_name = ? +''', [adm_name]) + for row in curs.all(): + self._handle_adm(*row) + + else: + LOGGER.info('Reloading all ADMS...') + + curs = db_conn.execute('''\ +SELECT data_model.name, adm_data.updated_at, adm_data.data +FROM adm_data + INNER JOIN data_model ON adm_data.enumeration = data_model.data_model_id +''') + for row in curs.all(): + self._handle_adm(*row) + + LOGGER.info('ADMS present for: %s', self._adms.names()) + + def _handle_adm(self, adm_name, timestamp, data): + LOGGER.info('Handling ADM: %s', adm_name) + LOGGER.info(type(data)) + # LOGGER.info(data.tos()) + + io_buffer = io.StringIO(data.tobytes().decode('utf-8')) + + self._adms.load_from_data(io_buffer) + LOGGER.info('Handling finished') + + +# SIGNALTON transmorgifier +TRANSMORGIFIER = Transmorgifier(config) \ No newline at end of file From 8a49c42651bb2b500112ea34d9c952eef4510669 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 18 Dec 2025 10:45:55 -0500 Subject: [PATCH 11/51] working version, added new profile default is internal transcoding --- anms-core/anms/shared/transmogrifier.py | 15 +++++++++------ docker-compose.yml | 8 ++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index 479c0d9b..7d15189e 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -31,6 +31,9 @@ import traceback import ace import io +import io +import json +import sqlalchemy config = ConfigBuilder.get_config() LOGGER = OpenSearchLogger(__name__, log_console=True) @@ -47,17 +50,17 @@ def __init__(self, args): LOGGER.info('Connecting to SQL DB at %s', args.db_uri) self._dbeng = sqlalchemy.create_engine(args.db_uri) self._adms = ace.AdmSet(cache_dir=False) - self.transcode = _transcode_internal - self.reload = _reload_internal + self.transcode = self._transcode_internal + self.reload = self._reload_internal self._adm_reload(None) else: # setting up the MQTT server instead - self.transcode = _transcode_mqtt - self.reload = _reload_mqtt + self.transcode = self._transcode_mqtt + self.reload = self._reload_mqtt def _transcode_mqtt(self, input): msg = json.dumps({'uri': input}) - logger.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) + LOGGER.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) def _transcode_internal(self, input): @@ -67,7 +70,7 @@ def _transcode_internal(self, input): res_obj['cbor'] = "" try: - req_obj = json.loads(msg.payload) + req_obj = input LOGGER.info('Request %s', req_obj) in_text = req_obj['uri'].strip() diff --git a/docker-compose.yml b/docker-compose.yml index 28f671ab..91bae174 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -134,6 +134,8 @@ services: - "${DB_PORT-5432}:5432" mqtt-broker: + profiles: + - external_transcode hostname: mqtt-broker image: ${DOCKER_IMAGE_PREFIX}mqtt-broker:${DOCKER_IMAGE_TAG} build: @@ -146,6 +148,8 @@ services: - "${MQTT_PORT:-1883}:1883" transcoder: + profiles: + - external_transcode hostname: transcoder image: ${DOCKER_IMAGE_PREFIX}transcoder:${DOCKER_IMAGE_TAG} build: @@ -159,6 +163,8 @@ services: condition: service_healthy aricodec: + profiles: + - external_transcode hostname: aricodec image: ${DOCKER_IMAGE_PREFIX}aricodec:${DOCKER_IMAGE_TAG} build: @@ -275,8 +281,6 @@ services: condition: service_started postgres: condition: service_healthy - mqtt-broker: - condition: service_healthy amp-manager: hostname: amp-manager From 14eda5f74bbb325c28272f81e385d5feff75fcb6 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 18 Dec 2025 17:00:12 -0500 Subject: [PATCH 12/51] working transcoder fixed issue with threading and DB --- anms-core/anms/init_adms.py | 14 +--- anms-core/anms/routes/adms/adm.py | 13 ++- anms-core/anms/routes/transcoder.py | 20 +++-- anms-core/anms/shared/mqtt_client.py | 21 ++--- anms-core/anms/shared/transmogrifier.py | 102 +++++++++++++++--------- 5 files changed, 97 insertions(+), 73 deletions(-) diff --git a/anms-core/anms/init_adms.py b/anms-core/anms/init_adms.py index 6453eccf..e648cd17 100644 --- a/anms-core/anms/init_adms.py +++ b/anms-core/anms/init_adms.py @@ -31,6 +31,7 @@ from anms.routes.adms.adm import handle_adm from anms.shared.config_utils import ConfigBuilder from anms.shared.opensearch_logger import OpenSearchLogger +from anms.shared.transmogrifier import TRANSMORGIFIER logger = OpenSearchLogger(__name__).logger @@ -67,17 +68,10 @@ async def import_adms(): logger.error('ADM %s handling failed: %s', adm_file.norm_name, err) logger.debug('%s', traceback.format_exc()) + # Notify the aricodec of startup - config = ConfigBuilder.get_config() - host = config.get('MQTT_HOST') - port = config.get('MQTT_PORT') - - logger.info('Connecting to MQTT broker %s to notify aricodec', host) - client = mqtt.client.Client() - client.connect(host, port) - msg = client.publish('aricodec/reload', b'') - msg.wait_for_publish() - client.disconnect() + TRANSMORGIFIER.reload() + logger.info('Startup finished') diff --git a/anms-core/anms/routes/adms/adm.py b/anms-core/anms/routes/adms/adm.py index 83049c4e..12091106 100644 --- a/anms-core/anms/routes/adms/adm.py +++ b/anms-core/anms/routes/adms/adm.py @@ -29,20 +29,17 @@ from pydantic import BaseModel import io import traceback -from typing import TextIO # Internal modules -from sqlalchemy import delete, select, and_ -from sqlalchemy.engine import Result -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import delete, and_ from anms.models.relational.adms import (adm_data, data_model_view) -from anms.models.relational.adms.data_model_view import DataModel as ADM from anms.routes.adms.adm_compare import (AdmCompare) from anms.shared.opensearch_logger import OpenSearchLogger -from anms.shared.mqtt_client import MQTT_CLIENT -from anms.models.relational import get_async_session, get_session + +from anms.shared.transmogrifier import TRANSMORGIFIER +from anms.models.relational import get_async_session from anms.components.schemas.adm import DataModelSchema import ace from camp.generators import (create_sql) @@ -296,7 +293,7 @@ async def update_adm(file: UploadFile, request: Request): if error_message: raise Exception(error_message) # Notify the transcoder - MQTT_CLIENT.publish('aricodec/reload', adm_file.norm_name) + TRANSMORGIFIER.reload(adm_file.norm_name) logger.info(f"{info_message} adm file: {file.filename} successfully") except Exception as err: logger.error(f"{sql_dialect} execution error: {err.args}") diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index 8cb7ba41..a5061648 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -96,8 +96,10 @@ async def transcoder_put_cbor_await(cbor: str): def _transcoder_put_cbor(input_cbor): transcoder_log_id = None + send_to_transcode = False with get_session() as session: curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==input_cbor, TranscoderLog.cbor==input_cbor)).first() + if curr_uri is None: c1 = TranscoderLog(input_string=input_cbor, parsed_as='pending') session.add(c1) @@ -105,13 +107,16 @@ def _transcoder_put_cbor(input_cbor): session.refresh(c1) transcoder_log_id = c1.transcoder_log_id session.commit() - status = "Submitted ARI to transcoder" - TRANSMORGIFIER.transcode(input_cbor) + send_to_transcode = True else: # the input_ari has already been submitted status = "ARI previously submitted, check log" transcoder_log_id = curr_uri.transcoder_log_id - + + if(send_to_transcode): + status = "Submitted ARI to transcoder" + TRANSMORGIFIER.transcode(input_cbor) + return {"id": transcoder_log_id, "status": status} @@ -142,6 +147,7 @@ def transcoder_incoming_str(input_ari: str): def _transcoder_put_str(input_ari: str): input_ari = input_ari.strip() transcoder_log_id = None + send_to_transcode = False with get_session() as session: curr_uri = TranscoderLog.query.filter(or_(TranscoderLog.input_string==input_ari,TranscoderLog.ari==input_ari, TranscoderLog.cbor==input_ari)).first() if curr_uri is None: @@ -151,17 +157,19 @@ def _transcoder_put_str(input_ari: str): session.refresh(c1) transcoder_log_id = c1.transcoder_log_id session.commit() - status = "Submitted ARI to transcoder" - TRANSMORGIFIER.transcode(input_ari) + send_to_transcode = True else: # the input_ari has already been submitted status = "ARI previously submitted, check log" transcoder_log_id = curr_uri.transcoder_log_id + + if(send_to_transcode): + status = "Submitted ARI to transcoder" + TRANSMORGIFIER.transcode(input_ari) return {"id": transcoder_log_id, "status": status} - # PUT /ui/incoming_send/str Body is str ARI to send to transcoder @router.put("/ui/incoming_send/str", status_code=status.HTTP_200_OK) async def transcoder_send_ari_str(eid: str, ari: str): diff --git a/anms-core/anms/shared/mqtt_client.py b/anms-core/anms/shared/mqtt_client.py index 149e8555..5bf8e124 100644 --- a/anms-core/anms/shared/mqtt_client.py +++ b/anms-core/anms/shared/mqtt_client.py @@ -41,16 +41,17 @@ def __init__(self, config): return # Create MQTT Client - client = mqtt.client.Client("Core_MQTT_Client", clean_session=False) - client.on_connect = self._on_connect - client.on_message = self._on_message - logger.info('Connecting to MQTT broker at %s:%s', host, port) - client.connect_async(host, port, keepalive=60) - self.client = client - checking_child = Thread(target=self._check_pending) - checking_child.daemon = True - checking_child.start() - client.loop_start() + if config.Transcoder != "Internal": + client = mqtt.client.Client("Core_MQTT_Client", clean_session=False) + client.on_connect = self._on_connect + client.on_message = self._on_message + logger.info('Connecting to MQTT broker at %s:%s', host, port) + client.connect_async(host, port, keepalive=60) + self.client = client + checking_child = Thread(target=self._check_pending) + checking_child.daemon = True + checking_child.start() + client.loop_start() def publish(self, *args, **kwargs): ''' If connected, pass through a publish request. ''' diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index 7d15189e..dc267204 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -24,7 +24,7 @@ from anms.shared.config import ConfigBuilder from anms.shared.logger import Logger -from anms.shared.mqtt_client import MQTT_CLIENT +import anms.shared.mqtt_client from anms.shared.opensearch_logger import OpenSearchLogger from anms.models.relational import get_session from anms.models.relational.transcoder_log import TranscoderLog @@ -36,7 +36,7 @@ import sqlalchemy config = ConfigBuilder.get_config() -LOGGER = OpenSearchLogger(__name__, log_console=True) +LOGGER = OpenSearchLogger(__name__, log_console=True).logger # depending on what the config is for core will either use a MQTT server to send off commands or @@ -44,36 +44,49 @@ class Transmorgifier: ''' The Transmogifier that can be configured to use an external or internal translator. ''' + # args = config def __init__(self, args): # if the transcoding in internal to core - if config.Transcoder == "internal": - LOGGER.info('Connecting to SQL DB at %s', args.db_uri) - self._dbeng = sqlalchemy.create_engine(args.db_uri) - self._adms = ace.AdmSet(cache_dir=False) + LOGGER.info(config.Transcoder) + if config.Transcoder == "Internal": + db_uri = f"postgresql://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}/{config.DB_CHROOT}" + LOGGER.info(f'Connecting to SQL DB at {db_uri}') + self._dbeng = sqlalchemy.create_engine(db_uri) + self.transcode = self._transcode_internal self.reload = self._reload_internal self._adm_reload(None) else: # setting up the MQTT server instead + self.MQTT_CLIENT = anms.shared.mqtt_client.MQTT_CLIENT self.transcode = self._transcode_mqtt self.reload = self._reload_mqtt def _transcode_mqtt(self, input): msg = json.dumps({'uri': input}) - LOGGER.info('PUBLISH to transcode/CoreFacing/Outgoing, msg = %s' % msg) - MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) + LOGGER.info(f'PUBLISH to transcode/CoreFacing/Outgoing, msg = {msg}') + self.MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) def _transcode_internal(self, input): + self._ace_transcode(input) + + # picking up any stray items that didnt get translated + pending_uris = TranscoderLog.query.filter_by(parsed_as='pending').all() + for entrys in pending_uris: + try: + self._ace_transcode(entrys.input_string) + except Exception as err: + LOGGER.error('Failed to process pending entry: %s', err) + + def _ace_transcode(self, input): # result object to fill in res_obj = {} res_obj['uri'] = "" res_obj['cbor'] = "" - + adms = ace.AdmSet() try: - req_obj = input - LOGGER.info('Request %s', req_obj) - - in_text = req_obj['uri'].strip() + LOGGER.info(f'Request {input}') + in_text = input.strip() res_obj['inputString'] = in_text in_lower = in_text.casefold() if in_lower.startswith('ari:0x') or in_lower.startswith('0x'): @@ -87,9 +100,9 @@ def _transcode_internal(self, input): in_bytes = ace.cborutil.from_hexstr(in_text) dec = ace.ari_cbor.Decoder() ari = dec.decode(io.BytesIO(in_bytes)) - LOGGER.debug('decoded as ARI %s', ari) - # ace.nickname.Converter(ace.nickname.Mode.FROM_NN, self._admsSession(self._dbeng), True)(ari) - ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, self._adms.db_session(), False)(ari) + LOGGER.debug(f'decoded as ARI {ari}') + # ace.nickname.Converter(ace.nickname.Mode.FROM_NN, admsSession(self._dbeng), True)(ari) + ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari) except Exception as err: raise RuntimeError(f"Error decoding from `{in_text}`: {err}") from err @@ -104,7 +117,7 @@ def _transcode_internal(self, input): out_text = buf.getvalue() if not out_text.startswith('ari:'): out_text = 'ari:' + out_text - LOGGER.debug('encoded as text %s', out_text) + LOGGER.debug(f'encoded as text {out_text}') except Exception as err: raise RuntimeError(f"Error encoding from {ari}: {err}") from err res_obj['uri'] = out_text @@ -116,8 +129,8 @@ def _transcode_internal(self, input): try: dec = ace.ari_text.Decoder() ari = dec.decode(io.StringIO(in_text)) - LOGGER.debug('decoded as ARI %s', ari) - ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, self._adms.db_session(), False)(ari) + LOGGER.debug(f'decoded as ARI {ari}') + ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari) except Exception as err: raise RuntimeError(f"Error decoding from `{in_text}`: {err}") from err @@ -130,7 +143,7 @@ def _transcode_internal(self, input): out_text = buf.getvalue() if not out_text.startswith('ari:'): out_text = 'ari:' + out_text - LOGGER.debug('encoded as text %s', out_text) + LOGGER.debug(f'encoded as text {out_text}') except Exception as err: raise RuntimeError(f"Error encoding from {ari}: {err}") from err @@ -143,37 +156,47 @@ def _transcode_internal(self, input): enc.encode(ari, buf) hex_str = ace.cborutil.to_hexstr(buf.getvalue()) - LOGGER.info('encoded as binary %s', hex_str) + LOGGER.info(f'encoded as binary {hex_str}') except Exception as err: raise RuntimeError(f"Error encoding from {ari}: {err}") from err res_obj['cbor'] = hex_str - except Exception as err: res_obj['ari'] = f'Failed to process: {err}' res_obj['parsedAs'] = 'ERROR' - LOGGER.error('Failed to process: %s', err) - LOGGER.info('Traceback:\n%s', traceback.format_exc()) - - LOGGER.info('Response %s', res_obj) - - # client.publish('transcode/CodexFacing/Outgoing', json.dumps(res_obj)) - # just log it back into the database - + LOGGER.error(f'Failed to process: {err}') + LOGGER.info(f'Traceback:\n{traceback.format_exc()}') + # store in transcoder database with get_session() as session: - session.query(TranscoderLog).filter(TranscoderLog.input_string == res_obj['inputString']). \ - update({ + session.query(TranscoderLog).filter(TranscoderLog.input_string == input).update({ 'parsed_as': res_obj['parsedAs'], 'ari': json.dumps(res_obj['ari']), 'cbor': res_obj['cbor'], 'uri': res_obj['uri'] }) - session.commit() + session.commit() + LOGGER.info(f'Response {res_obj}') + + # client.publish('transcode/CodexFacing/Outgoing', json.dumps(res_obj)) + # just log it back into the database + + + + def _reload_mqtt(self,adm_name=None): + config = ConfigBuilder.get_config() + host = config.get('MQTT_HOST') + port = config.get('MQTT_PORT') - def _reload_mqtt(self,adm_name): + LOGGER.info('Connecting to MQTT broker %s to notify aricodec' % host) + + msg = self.MQTT_CLIENT.publish('aricodec/reload', b'') + if adm_name: + msg = self.MQTT_CLIENT.publish('aricodec/reload', f'{adm_name}') + msg.wait_for_publish() + return - def _reload_internal(self, adm_name): + def _reload_internal(self, adm_name=None): try: self._adm_reload(adm_name) except Exception as err: @@ -204,17 +227,18 @@ def _adm_reload(self, adm_name): for row in curs.all(): self._handle_adm(*row) - LOGGER.info('ADMS present for: %s', self._adms.names()) + def _handle_adm(self, adm_name, timestamp, data): - LOGGER.info('Handling ADM: %s', adm_name) + LOGGER.info(f'Handling ADM:{adm_name}') LOGGER.info(type(data)) # LOGGER.info(data.tos()) io_buffer = io.StringIO(data.tobytes().decode('utf-8')) - - self._adms.load_from_data(io_buffer) + adms = ace.AdmSet() + adms.load_from_data(io_buffer) LOGGER.info('Handling finished') + LOGGER.info('ADMS present for: %s', adms.names()) # SIGNALTON transmorgifier From 337e9127ab653da5902c4d2d724e3ab6cc5175df Mon Sep 17 00:00:00 2001 From: David Edell Date: Fri, 19 Dec 2025 09:58:01 -0500 Subject: [PATCH 13/51] Automatically create GHCR images when a tag is created. This supplements the formal release process which also triggers publication. --- .github/workflows/publish_images.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish_images.yaml b/.github/workflows/publish_images.yaml index 49f9a341..250107d8 100644 --- a/.github/workflows/publish_images.yaml +++ b/.github/workflows/publish_images.yaml @@ -5,6 +5,8 @@ on: push: branches: - main + tags: + - 'v*' # Triggers the workflow for any tag starting with 'v', e.g., v1.0.0 release: types: [published] From 4170e901ec4636c53655575a0bb9cd76450eda24 Mon Sep 17 00:00:00 2001 From: David Edell Date: Fri, 19 Dec 2025 10:20:28 -0500 Subject: [PATCH 14/51] Fix ghcr name if triggered by tag --- .github/workflows/publish_images.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish_images.yaml b/.github/workflows/publish_images.yaml index 250107d8..4b482ff6 100644 --- a/.github/workflows/publish_images.yaml +++ b/.github/workflows/publish_images.yaml @@ -38,6 +38,8 @@ jobs: run: | if [[ "${{ github.event_name }}" == "release" ]]; then echo "DOCKER_IMAGE_TAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + elif [[ "${{ github.ref_type }}" == "tag" ]]; then + echo "DOCKER_IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV else echo "DOCKER_IMAGE_TAG=latest" >> $GITHUB_ENV fi From a055b23d4dde35b07e7e1217d3c9f69c8d017ae1 Mon Sep 17 00:00:00 2001 From: David Edell Date: Mon, 9 Feb 2026 15:56:06 -0500 Subject: [PATCH 15/51] Fixing grafana authnz/demo websocket proxy configuration. --- auth/demo/httpd.conf | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/auth/demo/httpd.conf b/auth/demo/httpd.conf index 2e4279f6..a8ba6778 100644 --- a/auth/demo/httpd.conf +++ b/auth/demo/httpd.conf @@ -507,9 +507,8 @@ SSLCACertificateFile /ammos/etc/pki/tls/certs/ammos-ca-bundle.crt # websockets get different proxy scheme - - ProxyPass "ws://grafana:3000/$1" - ProxyPassReverse "ws://grafana:3000/$1" + + ProxyPass "ws://grafana:3000/api/live" upgrade=websocket # Access the login page itself, all others are reverse-proxied From 2154cfcf97bc0020a0ddea027ab7cd65613f0f44 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 10 Feb 2026 17:03:18 -0500 Subject: [PATCH 16/51] removed services related to transcoder since they arent up by default --- anms-ui/config_ui_env.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/anms-ui/config_ui_env.js b/anms-ui/config_ui_env.js index 7fef23ba..855d851f 100644 --- a/anms-ui/config_ui_env.js +++ b/anms-ui/config_ui_env.js @@ -25,11 +25,9 @@ window.anms_env_config = { SERVICE_INFO: { names: [ "adminer","anms-core","authnz","amp-manager", - "aricodec","postgres", - "redis","mqtt-broker","transcoder", - "grafana","grafana-image-renderer" + "postgres","redis","grafana","grafana-image-renderer" ], normal_status: ["running","healthy"], error_status: ["not-running","unhealthy"] } -}; +}; \ No newline at end of file From 928c85912eb38a57f1c853bfa8d819bd61826fc0 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 12 Feb 2026 09:52:23 -0500 Subject: [PATCH 17/51] added --ignore flag for podman volume creaete to avoid name error --- create_volume.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/create_volume.sh b/create_volume.sh index 1d511de2..7d61fb5d 100755 --- a/create_volume.sh +++ b/create_volume.sh @@ -27,16 +27,18 @@ if [ -n "$DOCKER_CMD" ]; then elif command -v podman &> /dev/null; then echo "Podman is installed" DOCKER_CMD="podman" + EXTRA_FLAGS="--ignore" elif command -v docker &> /dev/null; then echo "Docker is installed" DOCKER_CMD="docker" + EXTRA_FLAGS="--ignore" else echo "Neither Docker nor Podman is installed" exit 1 fi -${DOCKER_CMD} volume create ${VOLNAME} +${DOCKER_CMD} volume create ${EXTRA_FLAGS} ${VOLNAME} # Delete our created volume if there is an error to prevent issues on retry trap '${DOCKER_CMD} volume rm ${VOLNAME}' ERR @@ -56,4 +58,4 @@ do done # creating socket volume -${DOCKER_CMD} volume create sockdir +${DOCKER_CMD} volume create ${EXTRA_FLAGS} sockdir From 73096f8f22a4f6f86763455ac42567d9a07803b9 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 19 Feb 2026 11:54:29 -0500 Subject: [PATCH 18/51] moved grafana db password to enviroment var --- .env | 8 +++++--- create_volume.sh | 1 - docker-compose.yml | 1 + grafana/provisioning/datasources/datasource.yml | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 23945427..ec10787e 100644 --- a/.env +++ b/.env @@ -7,8 +7,8 @@ COMPOSE_PROFILES=full,dev # Port Services; Uncomment below lines to override default mappings -#AUTHNZ_PORT=8084 -#AUTHNZ_HTTPS_PORT=8443 +AUTHNZ_PORT=8084 +AUTHNZ_HTTPS_PORT=8443 #OPENSEARCH_PORT1= #OPENSEARCH_PORT2= #OPENSEARCH_DASH_PORT= @@ -57,7 +57,7 @@ KIBANA_PORT=5601 ADMINER_PORT=8080 RENDERER_PORT=8081 RENDERER_HOST_PORT=grafana-image-renderer:${RENDERER_PORT} -ION_MGR_PORT=8089 +ION_MGR_PORT=9089 HTTP_PORT=80 # Path (or volume name) on the host @@ -66,3 +66,5 @@ HOST_SOCKDIR=sockdir CTR_SOCKDIR=/var/tmp/nm ADM_PATH=deps/dtnma-adms + +GRAFANA_PASS=grafana \ No newline at end of file diff --git a/create_volume.sh b/create_volume.sh index 7d61fb5d..2d6980ca 100755 --- a/create_volume.sh +++ b/create_volume.sh @@ -31,7 +31,6 @@ elif command -v podman &> /dev/null; then elif command -v docker &> /dev/null; then echo "Docker is installed" DOCKER_CMD="docker" - EXTRA_FLAGS="--ignore" else echo "Neither Docker nor Podman is installed" exit 1 diff --git a/docker-compose.yml b/docker-compose.yml index efc4ad97..05218d2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -237,6 +237,7 @@ services: GF_DATABASE_PATH: "" # Explicitly disable internal sqlite for clarity GF_PLUGINS_PREINSTALL: "yesoreyeram-infinity-datasource" GODEBUG: x509negativeserial=1 #fix for tls: failed to parse certificate from server: x509: negative serial number + GRAFANA_PASS: ${GRAFANA_PASS} grafana-image-renderer: hostname: grafana-image-renderer diff --git a/grafana/provisioning/datasources/datasource.yml b/grafana/provisioning/datasources/datasource.yml index ffaf3482..3d0d467e 100644 --- a/grafana/provisioning/datasources/datasource.yml +++ b/grafana/provisioning/datasources/datasource.yml @@ -34,7 +34,7 @@ datasources: database: amp_core user: grafana secureJsonData: - password: grafana + password: ${GRAFANA_PASS} database: amp_core version: 1 editable: true From a825c1266e746930187cc3e7cf2347033ed195b1 Mon Sep 17 00:00:00 2001 From: David Linko Date: Mon, 23 Feb 2026 16:30:52 -0500 Subject: [PATCH 19/51] moving adms init --- anms-core/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anms-core/docker-entrypoint.sh b/anms-core/docker-entrypoint.sh index ee19ab78..ad01f549 100644 --- a/anms-core/docker-entrypoint.sh +++ b/anms-core/docker-entrypoint.sh @@ -28,6 +28,6 @@ set -e PYTHON=${PYTHON:-python3} # initialize DB state -${PYTHON} -m anms.init_adms +# ${PYTHON} -m anms.init_adms exec ${PYTHON} -m anms.run_gunicorn From 1032a485ef0b0683cb49b7734f13236db384a115 Mon Sep 17 00:00:00 2001 From: David Edell Date: Tue, 3 Mar 2026 15:26:51 -0500 Subject: [PATCH 20/51] Updated REST APIs to return correct error codes, and updated dtnma-tools to bring in fixes for SQL concurrency causing reliability issues. --- anms-core/anms/routes/network_manager.py | 16 ++++++++-------- anms-core/anms/routes/transcoder.py | 16 ++++++++++------ deps/dtnma-tools | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/anms-core/anms/routes/network_manager.py b/anms-core/anms/routes/network_manager.py index d7735131..81d6c463 100644 --- a/anms-core/anms/routes/network_manager.py +++ b/anms-core/anms/routes/network_manager.py @@ -21,7 +21,7 @@ # the prime contract 80NM0018D0004 between the Caltech and NASA under # subcontract 1658085. # -from fastapi import APIRouter, status +from fastapi import APIRouter, status, HTTPException import requests from pydantic import BaseModel @@ -51,7 +51,7 @@ async def nm_get_version(): try: request = requests.get(url=url) except Exception: - return {} + raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail="ConnectTimeout") return request.json() @@ -63,7 +63,7 @@ def nm_get_agents(): try: request = requests.get(url=url) except Exception: - return -1 + raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail="ConnectTimeout") return request.json() @@ -90,20 +90,20 @@ def do_nm_put_hex_eid(eid: str, ari: str): url = nm_url + "/agents/eid/{}/send?form=hex".format(_prepare_url(eid)) logger.info('post to nm manager %s with eid %s and data %s' % (url, eid, ari)) - try: + try: request = requests.post(url=url, data=ari, headers={'Content-Type': 'text/plain'}, timeout=(2.0, 8.0) # 2s for manager to connect, 8s for it to respond ) except requests.exceptions.ConnectTimeout: - return status.HTTP_504_GATEWAY_TIMEOUT + raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail="ConnectTimeout") except requests.exceptions.ReadTimeout: - return status.HTTP_504_GATEWAY_TIMEOUT + raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail="ReadTimeout") except requests.exceptions.Timeout: - return status.HTTP_504_GATEWAY_TIMEOUT + raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail="Timeout") except Exception: - return status.HTTP_500_INTERNAL_SERVER_ERROR + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) return request.status_code diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index cc1b38fd..3a4bc23e 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -22,8 +22,7 @@ import time import asyncio -from fastapi import APIRouter, Depends -from fastapi import status +from fastapi import APIRouter, Depends, status, HTTPException from fastapi_pagination import Page, Params from fastapi_pagination.ext.async_sqlalchemy import paginate @@ -144,7 +143,7 @@ async def transcoder_put_await_str(input_ari: str): def transcoder_incoming_str(input_ari: str): return _transcoder_put_str(input_ari) -def _transcoder_put_str(input_ari: str): +def transcoder_put_str(input_ari: str): input_ari = input_ari.strip() transcoder_log_id = None send_to_transcode = False @@ -188,16 +187,21 @@ async def transcoder_send_ari_str(eid: str, ari: str): if info.parsed_as != "pending": break if retries <= 0: - return { "idinfo" : idinfo, "info" : info, "status" : 504 } + raise HTTPException(status_code=504, + detail={ "idinfo" : idinfo, "info" : info, "status" : "transcoder timeout" }) + retries -= 1 if info.parsed_as == "ERROR": - return { "idinfo" : idinfo, "info" : info, "status" : 500 } + raise HTTPException(status_code=500, + detail={ "idinfo" : idinfo, "info" : info, "status" : 500 }) # Publish state = do_nm_put_hex_eid( eid, info.cbor ) - return { "idinfo" : idinfo, "info" : info, "status" : state } + except HTTPException as e: + e.detail = { "idinfo" : idinfo, "info" : info, "status" : e.status_code } + raise e except Exception as e: logger.exception(e) return status.HTTP_500_INTERNAL_SERVER_ERROR diff --git a/deps/dtnma-tools b/deps/dtnma-tools index 58e55575..20a47469 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit 58e5557570064b6fc2a1719a0eb9f90fa15c94d6 +Subproject commit 20a47469690c1be8b7a1a5114003c7bcafa3f20e From c685598523ccab9c9d4f5996f28320366d3a9b37 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 5 Mar 2026 13:04:36 -0500 Subject: [PATCH 21/51] moved init adms logic into transcoder --- anms-core/anms/routes/adms/adm.py | 65 ----------------- anms-core/anms/shared/transmogrifier.py | 96 +++++++++++++++++++++++-- anms-core/docker-entrypoint.sh | 3 - 3 files changed, 92 insertions(+), 72 deletions(-) diff --git a/anms-core/anms/routes/adms/adm.py b/anms-core/anms/routes/adms/adm.py index 12091106..c74b4e5e 100644 --- a/anms-core/anms/routes/adms/adm.py +++ b/anms-core/anms/routes/adms/adm.py @@ -127,71 +127,6 @@ async def remove_adm(enumeration: int, namespace:str): raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST, detail = f"ADM ENUM:{enumeration} in NAMESPACE {namespace} not a known ADM") - -async def handle_adm(admset: ace.AdmSet, adm_file: ace.models.AdmModule, session, replace=True): - ''' Process a received and decoded ADM into the ANMS DB. - - :param replace: If true and the ADM exists it will be checked and replaced. - :return: A list of issues with the ADM, which is empty if successful. - ''' - logger.info("Adm name: %s", adm_file.norm_name) - data_model_view = await DataModel.get(adm_file.ns_model_enum,adm_file.ns_org_name ) - if data_model_view: - if not replace: - logger.info('Not replacing existing ADM name %s', adm_file.norm_name) - return [] - data_rec = None - async with get_async_session() as session: - data_rec,_ = await AdmData.get(data_model_view.data_model_id,session) - - if data_rec: - # Compare old and new contents - logger.info("Checking existing ADM name %s", adm_file.norm_name) - old_adm = admset.load_from_data(io.BytesIO(data_rec.data), del_dupe=False) - comp = AdmCompare(admset) - if not comp.compare_adms(old_adm, adm_file): - issues = comp.get_errors() - else: - issues = [f"Updating existing adm is not allowed yet"] - return issues - - logger.info("Inserting ADM name %s", adm_file.norm_name) - - # Use CAmPython to generate sql - out_path = "" # This is empty string since we don't need to write the generated sql to a file - sql_dialect = 'pgsql' - writer = create_sql.Writer(admset, adm_file, out_path, sql_dialect) - string_buffer = io.StringIO() - writer.write(string_buffer) - - # execute generated Sql - queries = string_buffer.getvalue() - try: - await session.execute(queries) - await session.commit() - except Exception as err: - logger.error(f"{sql_dialect} execution error: {err.args}") - logger.debug('%s', traceback.format_exc()) - raise - - # Save the adm file of the new adm - - - buf = io.StringIO() - ace.adm_yang.Encoder().encode(adm_file, buf) - ret_dm = await DataModel.get(adm_file.ns_model_enum, adm_file.ns_org_name, session) - - # Write the encoded string data to the BytesIO object - bytes_io = io.BytesIO() - bytes_io.write(buf.getvalue().encode('utf-8')) - # Reset the pointer to the beginning - bytes_io.seek(0) - data = {"enumeration":ret_dm.data_model_id, "data": bytes_io.getvalue()} - await AdmData.add_data(data, session) - - return [] - - @router.post("/", status_code=status.HTTP_201_CREATED, responses={400: {"model": RequestError}, 405: {"model": UpdateAdmError}, 500: {"model": RequestError}}) async def update_adm(file: UploadFile, request: Request): diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index dc267204..16a9f3cd 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -21,13 +21,17 @@ # the prime contract 80NM0018D0004 between the Caltech and NASA under # subcontract 1658085. - +from camp.generators import (create_sql) from anms.shared.config import ConfigBuilder -from anms.shared.logger import Logger +import asyncio import anms.shared.mqtt_client from anms.shared.opensearch_logger import OpenSearchLogger from anms.models.relational import get_session +from anms.models.relational import get_async_session from anms.models.relational.transcoder_log import TranscoderLog +from anms.models.relational.adms import (adm_data, data_model_view) +from anms.routes.adms.adm_compare import (AdmCompare) + import traceback import ace import io @@ -40,7 +44,7 @@ # depending on what the config is for core will either use a MQTT server to send off commands or -# use an internal +# use an ACE internally to translate class Transmorgifier: ''' The Transmogifier that can be configured to use an external or internal translator. ''' @@ -48,11 +52,13 @@ class Transmorgifier: def __init__(self, args): # if the transcoding in internal to core LOGGER.info(config.Transcoder) + self.AdmData = adm_data.AdmData + self.DataModel = data_model_view.DataModel if config.Transcoder == "Internal": db_uri = f"postgresql://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}/{config.DB_CHROOT}" LOGGER.info(f'Connecting to SQL DB at {db_uri}') self._dbeng = sqlalchemy.create_engine(db_uri) - + asyncio.run(self._load_default_adms()) self.transcode = self._transcode_internal self.reload = self._reload_internal self._adm_reload(None) @@ -61,6 +67,88 @@ def __init__(self, args): self.MQTT_CLIENT = anms.shared.mqtt_client.MQTT_CLIENT self.transcode = self._transcode_mqtt self.reload = self._reload_mqtt + self.reload() + + async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, session, replace=True): + ''' Process a received and decoded ADM into the ANMS DB. + + :param replace: If true and the ADM exists it will be checked and replaced. + :return: A list of issues with the ADM, which is empty if successful. + ''' + LOGGER.info("Adm name: %s", adm_file.norm_name) + data_model_view = await self.DataModel.get(adm_file.ns_model_enum,adm_file.ns_org_name ) + if data_model_view: + if not replace: + LOGGER.info('Not replacing existing ADM name %s', adm_file.norm_name) + return [] + data_rec = None + async with get_async_session() as session: + data_rec,_ = await self.AdmData.get(data_model_view.data_model_id,session) + + if data_rec: + # Compare old and new contents + LOGGER.info("Checking existing ADM name %s", adm_file.norm_name) + old_adm = admset.load_from_data(io.BytesIO(data_rec.data), del_dupe=False) + comp = AdmCompare(admset) + if not comp.compare_adms(old_adm, adm_file): + issues = comp.get_errors() + else: + issues = [f"Updating existing adm is not allowed yet"] + return issues + + LOGGER.info("Inserting ADM name %s", adm_file.norm_name) + + # Use CAmPython to generate sql + out_path = "" # This is empty string since we don't need to write the generated sql to a file + sql_dialect = 'pgsql' + writer = create_sql.Writer(admset, adm_file, out_path, sql_dialect) + string_buffer = io.StringIO() + writer.write(string_buffer) + + # execute generated Sql + queries = string_buffer.getvalue() + try: + await session.execute(queries) + await session.commit() + except Exception as err: + LOGGER.error(f"{sql_dialect} execution error: {err.args}") + LOGGER.debug('%s', traceback.format_exc()) + raise + + # Save the adm file of the new adm + + + buf = io.StringIO() + ace.adm_yang.Encoder().encode(adm_file, buf) + ret_dm = await self.DataModel.get(adm_file.ns_model_enum, adm_file.ns_org_name, session) + + # Write the encoded string data to the BytesIO object + bytes_io = io.BytesIO() + bytes_io.write(buf.getvalue().encode('utf-8')) + # Reset the pointer to the beginning + bytes_io.seek(0) + data = {"enumeration":ret_dm.data_model_id, "data": bytes_io.getvalue()} + await self.AdmData.add_data(data, session) + + return [] + + async def _load_default_adms(self): + admset = ace.AdmSet(cache_dir=False) + admset.load_default_dirs() + issues = ace.Checker(admset.db_session()).check() + for iss in issues: + LOGGER.error('ADM issue %s', iss) + + for adm_file in admset: + try: + LOGGER.info('ADM %s handling started', adm_file.norm_name) + async with get_async_session() as db_sess: + await self.handle_adm(admset, adm_file, db_sess, replace=False) + LOGGER.info('ADM %s handling finished', adm_file.norm_name) + except Exception as err: + # The function already logged any SQL issue at error severity + LOGGER.error('ADM %s handling failed: %s', adm_file.norm_name, err) + LOGGER.debug('%s', traceback.format_exc()) def _transcode_mqtt(self, input): msg = json.dumps({'uri': input}) diff --git a/anms-core/docker-entrypoint.sh b/anms-core/docker-entrypoint.sh index ad01f549..885b3de9 100644 --- a/anms-core/docker-entrypoint.sh +++ b/anms-core/docker-entrypoint.sh @@ -27,7 +27,4 @@ set -e PYTHON=${PYTHON:-python3} -# initialize DB state -# ${PYTHON} -m anms.init_adms - exec ${PYTHON} -m anms.run_gunicorn From f817d70fbaf39b3f623cd394e31216561c526047 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 5 Mar 2026 13:24:41 -0500 Subject: [PATCH 22/51] added new folder for adding new ADMS in core --- README.md | 2 +- anms.Containerfile | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b3e8cb0..2d8271f7 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ point. With ANMS running, go to `localhost:8080` and log in to the database with ### ADM and Agent Updates -Changes to ADMs are handled on the Manager by uploading a new version of the ADM via the Web UI. +By default ANMS starts with the ADMs defined in `deps/dtnma-adms` and ADMs added to `anms-core/extra_adms`. Changes to and adding new ADMs are handled on the Manager by uploading a new version of the ADM via the Web UI or the REST POST endpoint `http://localhost:5555/adms/`. The manager will then be able to use the new ADM. Changes to a test Agent are more complicated, and require auto-generated C sources built into the ION source tree. diff --git a/anms.Containerfile b/anms.Containerfile index ef08af1d..a07dd9b2 100644 --- a/anms.Containerfile +++ b/anms.Containerfile @@ -259,6 +259,9 @@ COPY anms-core/anms/agent_parameter.json /usr/local/share/anms/agent_parameter.j RUN touch /usr/local/share/anms/alerts.json RUN chmod go+w /usr/local/share/anms/alerts.json +# adding extra ADMS added before build +COPY anms-core/extra_adms /usr/local/share/ace/adms + RUN setcap cap_net_raw=ep /usr/bin/ping COPY --chmod=755 anms-core/docker-entrypoint.sh /usr/local/bin/ # Remaining commands as the local user From 1cbcf1012d168e8eb10a3daf7d3cb1b32eb532a6 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 5 Mar 2026 13:48:42 -0500 Subject: [PATCH 23/51] added gitignore to extra_adms to avoid extra adms be added to main --- anms-core/extra_adms/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 anms-core/extra_adms/.gitignore diff --git a/anms-core/extra_adms/.gitignore b/anms-core/extra_adms/.gitignore new file mode 100644 index 00000000..9ce5fab0 --- /dev/null +++ b/anms-core/extra_adms/.gitignore @@ -0,0 +1,3 @@ +#ignoring any added ADMs to this file to avoid uploading test or unwanted ADMs +* +!.gitignore \ No newline at end of file From 15052f64dd1e81c81b9c6ebdf210493e886d2e1f Mon Sep 17 00:00:00 2001 From: Brian Sipos Date: Fri, 6 Mar 2026 09:11:17 -0500 Subject: [PATCH 24/51] Add REFDA startup to allow ipn-scheme managers all access --- testenv.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/testenv.Containerfile b/testenv.Containerfile index 2b861e24..6cea562b 100644 --- a/testenv.Containerfile +++ b/testenv.Containerfile @@ -175,6 +175,7 @@ RUN systemctl enable ion bpecho@4 refda-ion && \ mkdir -p /var/run/ion # Runtime config for this container +COPY deps/dtnma-tools/integration-test-ion/startup.uri /etc/refda/startup.uri COPY deps/test-ion-configs/agent-2.rc /etc/ion/node-2.rc COPY deps/test-ion-configs/agent-3.rc /etc/ion/node-3.rc From 67003a7026dac56a3663db820c497da306f015a7 Mon Sep 17 00:00:00 2001 From: David Edell Date: Fri, 6 Mar 2026 09:17:42 -0500 Subject: [PATCH 25/51] Updating all deps references. --- deps/dtnma-ace | 2 +- deps/dtnma-adms | 2 +- deps/dtnma-camp | 2 +- deps/dtnma-tools | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deps/dtnma-ace b/deps/dtnma-ace index d7aa3cea..b303fd3a 160000 --- a/deps/dtnma-ace +++ b/deps/dtnma-ace @@ -1 +1 @@ -Subproject commit d7aa3ceabf750b4579e64f1c89911ab4c1d5d980 +Subproject commit b303fd3aff45070619e9fd273dbc3506f4dc597f diff --git a/deps/dtnma-adms b/deps/dtnma-adms index 2b95b67d..e9b600d2 160000 --- a/deps/dtnma-adms +++ b/deps/dtnma-adms @@ -1 +1 @@ -Subproject commit 2b95b67d42fde342a6c5459ee8b586da9b67abec +Subproject commit e9b600d2dfdf965dec71e86ffcb5932fd964b803 diff --git a/deps/dtnma-camp b/deps/dtnma-camp index d4dc7c27..0813bc49 160000 --- a/deps/dtnma-camp +++ b/deps/dtnma-camp @@ -1 +1 @@ -Subproject commit d4dc7c27430254177e2bb056399d2c321e7f95c4 +Subproject commit 0813bc49bf9ae859bf67521077f923574d18a28e diff --git a/deps/dtnma-tools b/deps/dtnma-tools index 20a47469..cd454d04 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit 20a47469690c1be8b7a1a5114003c7bcafa3f20e +Subproject commit cd454d0467b2b2c9422b3646006a5445e2da5514 From b19e3330a5916aa301ced31d236645f24db45892 Mon Sep 17 00:00:00 2001 From: David Edell Date: Fri, 6 Mar 2026 10:43:10 -0500 Subject: [PATCH 26/51] Force CI build to run with no-cache --- .github/workflows/build-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 8577e8aa..ca63a407 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -43,9 +43,9 @@ jobs: DOCKER_IMAGE_TAG=${{ github.sha }} echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >> $GITHUB_ENV - name: Build ANMS - run: ${DOCKER_CMD} compose ${ANMS_COMPOSE_OPTS} build + run: ${DOCKER_CMD} compose ${ANMS_COMPOSE_OPTS} build --no-cache - name: Build Agents - run: ${DOCKER_CMD} compose ${TESTENV_COMPOSE_OPTS} build + run: ${DOCKER_CMD} compose ${TESTENV_COMPOSE_OPTS} build --no-cache - name: Build Volume run: | ./create_volume.sh ./puppet/modules/apl_test/files/anms/tls From 1e7860e5f0bddc71a307e8ce5693a8e074edced7 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 10 Mar 2026 09:11:30 -0400 Subject: [PATCH 27/51] moved init logic to avoid loop startup error --- anms-core/anms/init_adms.py | 81 ------------------------- anms-core/anms/run_gunicorn.py | 3 + anms-core/anms/shared/transmogrifier.py | 6 +- 3 files changed, 4 insertions(+), 86 deletions(-) delete mode 100644 anms-core/anms/init_adms.py diff --git a/anms-core/anms/init_adms.py b/anms-core/anms/init_adms.py deleted file mode 100644 index e648cd17..00000000 --- a/anms-core/anms/init_adms.py +++ /dev/null @@ -1,81 +0,0 @@ -# -# Copyright (c) 2023 The Johns Hopkins University Applied Physics -# Laboratory LLC. -# -# This file is part of the Asynchronous Network Management System (ANMS). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This work was performed for the Jet Propulsion Laboratory, California -# Institute of Technology, sponsored by the United States Government under -# the prime contract 80NM0018D0004 between the Caltech and NASA under -# subcontract 1658085. -# -''' A separate entrypoint to initialize the ADMs database from filesystem. -''' -import asyncio -import sys -import traceback -import sqlalchemy.exc -from paho import mqtt -from ace import AdmSet, Checker -from anms.models.relational import get_async_session -from anms.routes.adms.adm import handle_adm -from anms.shared.config_utils import ConfigBuilder -from anms.shared.opensearch_logger import OpenSearchLogger -from anms.shared.transmogrifier import TRANSMORGIFIER - - -logger = OpenSearchLogger(__name__).logger - - -async def import_adms(): - ''' Scan filesystem for ADMs and import them if needed. - If they are pre-existing there is no change - ''' - # Wait for the DB to be accessible - while True: - try: - async with get_async_session() as db_sess: - await db_sess.execute("SELECT 1") - break - except (ConnectionError, sqlalchemy.exc.OperationalError): - logger.info('Waiting for DB to be accessible...') - await asyncio.sleep(1) - - admset = AdmSet(cache_dir=False) - admset.load_default_dirs() - issues = Checker(admset.db_session()).check() - for iss in issues: - logger.error('ADM issue %s', iss) - - for adm_file in admset: - try: - logger.info('ADM %s handling started', adm_file.norm_name) - async with get_async_session() as db_sess: - await handle_adm(admset, adm_file, db_sess, replace=False) - logger.info('ADM %s handling finished', adm_file.norm_name) - except Exception as err: - # The function already logged any SQL issue at error severity - logger.error('ADM %s handling failed: %s', adm_file.norm_name, err) - logger.debug('%s', traceback.format_exc()) - - - # Notify the aricodec of startup - TRANSMORGIFIER.reload() - - - logger.info('Startup finished') - - -if __name__ == '__main__': - asyncio.run(import_adms()) - sys.exit(0) diff --git a/anms-core/anms/run_gunicorn.py b/anms-core/anms/run_gunicorn.py index db083e6c..fda6167a 100644 --- a/anms-core/anms/run_gunicorn.py +++ b/anms-core/anms/run_gunicorn.py @@ -26,7 +26,9 @@ import ssl import sys import gunicorn.app.base +import asyncio +from anms.shared.transmogrifier import TRANSMORGIFIER from anms.shared.config import ConfigBuilder from anms.shared.opensearch_logger import OpenSearchLogger @@ -78,6 +80,7 @@ def on_starting(server): if __name__ == "__main__": + asyncio.run(TRANSMORGIFIER.load_default_adms()) # NOTE (for MAC users) export OBJC_DISABLE_INITIALIZE_FORK_SAFETY = YES before running this file os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES" StandaloneApplication().run() diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index 16a9f3cd..29d5f932 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -58,7 +58,6 @@ def __init__(self, args): db_uri = f"postgresql://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}/{config.DB_CHROOT}" LOGGER.info(f'Connecting to SQL DB at {db_uri}') self._dbeng = sqlalchemy.create_engine(db_uri) - asyncio.run(self._load_default_adms()) self.transcode = self._transcode_internal self.reload = self._reload_internal self._adm_reload(None) @@ -116,8 +115,6 @@ async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, s raise # Save the adm file of the new adm - - buf = io.StringIO() ace.adm_yang.Encoder().encode(adm_file, buf) ret_dm = await self.DataModel.get(adm_file.ns_model_enum, adm_file.ns_org_name, session) @@ -132,7 +129,7 @@ async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, s return [] - async def _load_default_adms(self): + async def load_default_adms(self): admset = ace.AdmSet(cache_dir=False) admset.load_default_dirs() issues = ace.Checker(admset.db_session()).check() @@ -189,7 +186,6 @@ def _ace_transcode(self, input): dec = ace.ari_cbor.Decoder() ari = dec.decode(io.BytesIO(in_bytes)) LOGGER.debug(f'decoded as ARI {ari}') - # ace.nickname.Converter(ace.nickname.Mode.FROM_NN, admsSession(self._dbeng), True)(ari) ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari) except Exception as err: From 3ee0d50eab95f387b789abc82b2617726d172810 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 10 Mar 2026 16:35:33 -0400 Subject: [PATCH 28/51] loading default ADMs controlled by a new route --- README.md | 2 +- anms-core/anms/routes/ARIs/ari.py | 2 -- anms-core/anms/routes/adms/adm.py | 5 +++++ anms-core/anms/run_gunicorn.py | 3 --- anms-core/anms/shared/transmogrifier.py | 17 ++++++++-------- anms-ui/public/app/components/adm/Adm.vue | 5 ++++- anms-ui/public/app/shared/api_adm.js | 7 ++++++- anms-ui/public/app/store/modules/adm.js | 13 ++++++++++++ anms-ui/server/components/adms.js | 24 +++++++++++++++++++++++ anms-ui/server/core/routes.js | 2 ++ 10 files changed, 64 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2d8271f7..7cbd1b79 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ point. With ANMS running, go to `localhost:8080` and log in to the database with ### ADM and Agent Updates -By default ANMS starts with the ADMs defined in `deps/dtnma-adms` and ADMs added to `anms-core/extra_adms`. Changes to and adding new ADMs are handled on the Manager by uploading a new version of the ADM via the Web UI or the REST POST endpoint `http://localhost:5555/adms/`. +By default after building the system, ANMS starts with the ADMs defined in `deps/dtnma-adms` and ADMs added to `anms-core/extra_adms`. Changes to and adding new ADMs are handled on the Manager by uploading a new version of the ADM via the Web UI or the REST POST endpoint `http://localhost:5555/adms/`. The manager will then be able to use the new ADM. Changes to a test Agent are more complicated, and require auto-generated C sources built into the ION source tree. diff --git a/anms-core/anms/routes/ARIs/ari.py b/anms-core/anms/routes/ARIs/ari.py index 23719c0e..9534dfc6 100644 --- a/anms-core/anms/routes/ARIs/ari.py +++ b/anms-core/anms/routes/ARIs/ari.py @@ -22,7 +22,6 @@ # subcontract 1658085. # import asyncio -from functools import cache from typing import List from fastapi import Depends, APIRouter @@ -36,7 +35,6 @@ from anms.models.relational import get_async_session from anms.models.relational.actual_parameter import ActualParameter from anms.models.relational.ari import ARI -from anms.models.relational.adms.data_model_view import DataModel from anms.models.relational.formal_parameter import FormalParameter diff --git a/anms-core/anms/routes/adms/adm.py b/anms-core/anms/routes/adms/adm.py index c74b4e5e..b13598da 100644 --- a/anms-core/anms/routes/adms/adm.py +++ b/anms-core/anms/routes/adms/adm.py @@ -126,6 +126,11 @@ async def remove_adm(enumeration: int, namespace:str): logger.debug(f"ADM ENUM:{enumeration} in NAMESPACE {namespace} not a known ADM") raise HTTPException(status_code = status.HTTP_400_BAD_REQUEST, detail = f"ADM ENUM:{enumeration} in NAMESPACE {namespace} not a known ADM") +@router.post("/load_default", status_code=status.HTTP_201_CREATED) +async def load_default_adm(): + await TRANSMORGIFIER.load_default_adms() + response = JSONResponse(status_code=status.HTTP_200_OK, content={"message": "Initilized default ADMs", "error_details": ""}) + return response @router.post("/", status_code=status.HTTP_201_CREATED, responses={400: {"model": RequestError}, 405: {"model": UpdateAdmError}, 500: {"model": RequestError}}) diff --git a/anms-core/anms/run_gunicorn.py b/anms-core/anms/run_gunicorn.py index fda6167a..db083e6c 100644 --- a/anms-core/anms/run_gunicorn.py +++ b/anms-core/anms/run_gunicorn.py @@ -26,9 +26,7 @@ import ssl import sys import gunicorn.app.base -import asyncio -from anms.shared.transmogrifier import TRANSMORGIFIER from anms.shared.config import ConfigBuilder from anms.shared.opensearch_logger import OpenSearchLogger @@ -80,7 +78,6 @@ def on_starting(server): if __name__ == "__main__": - asyncio.run(TRANSMORGIFIER.load_default_adms()) # NOTE (for MAC users) export OBJC_DISABLE_INITIALIZE_FORK_SAFETY = YES before running this file os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES" StandaloneApplication().run() diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index 29d5f932..b4ed70fb 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -52,8 +52,8 @@ class Transmorgifier: def __init__(self, args): # if the transcoding in internal to core LOGGER.info(config.Transcoder) - self.AdmData = adm_data.AdmData - self.DataModel = data_model_view.DataModel + self.adm_data = adm_data.AdmData + self.data_model = data_model_view.DataModel if config.Transcoder == "Internal": db_uri = f"postgresql://{config.DB_USER}:{config.DB_PASS}@{config.DB_HOST}/{config.DB_CHROOT}" LOGGER.info(f'Connecting to SQL DB at {db_uri}') @@ -66,7 +66,6 @@ def __init__(self, args): self.MQTT_CLIENT = anms.shared.mqtt_client.MQTT_CLIENT self.transcode = self._transcode_mqtt self.reload = self._reload_mqtt - self.reload() async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, session, replace=True): ''' Process a received and decoded ADM into the ANMS DB. @@ -75,14 +74,14 @@ async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, s :return: A list of issues with the ADM, which is empty if successful. ''' LOGGER.info("Adm name: %s", adm_file.norm_name) - data_model_view = await self.DataModel.get(adm_file.ns_model_enum,adm_file.ns_org_name ) + data_model_view = await self.data_model.get(adm_file.ns_model_enum,adm_file.ns_org_name ) if data_model_view: if not replace: LOGGER.info('Not replacing existing ADM name %s', adm_file.norm_name) return [] data_rec = None async with get_async_session() as session: - data_rec,_ = await self.AdmData.get(data_model_view.data_model_id,session) + data_rec,_ = await self.adm_data.get(data_model_view.data_model_id,session) if data_rec: # Compare old and new contents @@ -117,7 +116,7 @@ async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, s # Save the adm file of the new adm buf = io.StringIO() ace.adm_yang.Encoder().encode(adm_file, buf) - ret_dm = await self.DataModel.get(adm_file.ns_model_enum, adm_file.ns_org_name, session) + ret_dm = await self.data_model.get(adm_file.ns_model_enum, adm_file.ns_org_name, session) # Write the encoded string data to the BytesIO object bytes_io = io.BytesIO() @@ -125,10 +124,10 @@ async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, s # Reset the pointer to the beginning bytes_io.seek(0) data = {"enumeration":ret_dm.data_model_id, "data": bytes_io.getvalue()} - await self.AdmData.add_data(data, session) + await self.adm_data.add_data(data, session) return [] - + async def load_default_adms(self): admset = ace.AdmSet(cache_dir=False) admset.load_default_dirs() @@ -147,6 +146,8 @@ async def load_default_adms(self): LOGGER.error('ADM %s handling failed: %s', adm_file.norm_name, err) LOGGER.debug('%s', traceback.format_exc()) + self.reload() + def _transcode_mqtt(self, input): msg = json.dumps({'uri': input}) LOGGER.info(f'PUBLISH to transcode/CoreFacing/Outgoing, msg = {msg}') diff --git a/anms-ui/public/app/components/adm/Adm.vue b/anms-ui/public/app/components/adm/Adm.vue index 438aef4a..682b7fa0 100644 --- a/anms-ui/public/app/components/adm/Adm.vue +++ b/anms-ui/public/app/components/adm/Adm.vue @@ -108,13 +108,16 @@ export default { }, async mounted() { if (!this.hasAdms) { + await this.loadAdms(); await this.getAdms(); } }, methods: { ...mapActions("adm", { getAdms: "getAdms", - uploadAdm: "uploadAdm" + uploadAdm: "uploadAdm", + loadAdms: "loadAdms" + }), download(adm){ let json = {}; diff --git a/anms-ui/public/app/shared/api_adm.js b/anms-ui/public/app/shared/api_adm.js index 00dfb1fa..a58f3ad7 100644 --- a/anms-ui/public/app/shared/api_adm.js +++ b/anms-ui/public/app/shared/api_adm.js @@ -53,8 +53,13 @@ const apiUpdateAdm = async (file) => { ); }; +const apiLoadAdm = async () => { + return axios.post(adm_url+"/load_default", {headers: {accept: 'application/json'}}) +}; + export default { apiGetAdms, apiGetAdm, - apiUpdateAdm + apiUpdateAdm, + apiLoadAdm }; diff --git a/anms-ui/public/app/store/modules/adm.js b/anms-ui/public/app/store/modules/adm.js index 111b4982..923eed81 100644 --- a/anms-ui/public/app/store/modules/adm.js +++ b/anms-ui/public/app/store/modules/adm.js @@ -72,6 +72,19 @@ export default { }); }, + async loadAdms({ state, commit }){ + commit('loading', true); + commit('requestError', ""); + let sleep = (time) => new Promise((resolve) => setTimeout(resolve, time)); + api_adm.apiLoadAdm() + .catch(function (error) { + sleep(1000).then(() => { + commit('adms', []); + commit('requestError', error); + commit('loading', false); + }) + }); + }, async uploadAdm({ state, commit }, adm_file){ //commit('loading', true); commit('requestError', ""); diff --git a/anms-ui/server/components/adms.js b/anms-ui/server/components/adms.js index 419a9936..644676a6 100644 --- a/anms-ui/server/components/adms.js +++ b/anms-ui/server/components/adms.js @@ -131,5 +131,29 @@ } return res.status(response.status).json(response.data); }; + exports.loadDefault = async function (req, res, next) { + const usersReqHeader = utils.createAuthenticationHeader(req); + let requestUrl = utils.generateAnmsCoreUrl(["adms","load_default"]); + console.info("loadDefault requestUrl: ", requestUrl); + + const headers = { + ...usersReqHeader, + 'Content-Type': 'multipart/form-data' + }; + const response = await axios.post(requestUrl, + { + headers + } + ).catch(function (error) { + console.error("Upload file error: ", error.response.statusText) + return error.response + }); + if (_.isNil(response) || _.isNil(response.data) || _.isNil(response.data.message)) { + response.status = 500; + console.error(response); + response.data = {"message": "Internal Server Error"}; + } + return res.status(response.status).json(response.data); + }; })(); diff --git a/anms-ui/server/core/routes.js b/anms-ui/server/core/routes.js index d4575ec5..fb9a1b7f 100755 --- a/anms-ui/server/core/routes.js +++ b/anms-ui/server/core/routes.js @@ -102,6 +102,8 @@ router.get('/core/adms', userLimiter, adms.getAll); router.get('/core/adms/:adm_enum/:namespace', adms.getOne); router.post('/core/adms', userLimiter, upload.single('adm'), adms.upload); + router.post('/core/adms/load_default', userLimiter, upload.single('adm'), adms.loadDefault); + // ---- Agents Routes ---- // const agents = require('../components/registeredAgents'); From 691307124c1e2f988936465f656fa7f76b648c22 Mon Sep 17 00:00:00 2001 From: David Edell Date: Fri, 13 Mar 2026 12:39:40 -0400 Subject: [PATCH 29/51] Revert "Force CI build to run with no-cache" This reverts commit b19e3330a5916aa301ced31d236645f24db45892. --- .github/workflows/build-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index ca63a407..8577e8aa 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -43,9 +43,9 @@ jobs: DOCKER_IMAGE_TAG=${{ github.sha }} echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >> $GITHUB_ENV - name: Build ANMS - run: ${DOCKER_CMD} compose ${ANMS_COMPOSE_OPTS} build --no-cache + run: ${DOCKER_CMD} compose ${ANMS_COMPOSE_OPTS} build - name: Build Agents - run: ${DOCKER_CMD} compose ${TESTENV_COMPOSE_OPTS} build --no-cache + run: ${DOCKER_CMD} compose ${TESTENV_COMPOSE_OPTS} build - name: Build Volume run: | ./create_volume.sh ./puppet/modules/apl_test/files/anms/tls From 3e3291e2029fd0b64d3615dab731d3c3ec261a86 Mon Sep 17 00:00:00 2001 From: David Edell Date: Tue, 17 Mar 2026 01:50:43 -0400 Subject: [PATCH 30/51] Address SonarQube documentation warning. --- anms-core/anms/routes/transcoder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index 3a4bc23e..8e26ac98 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -170,7 +170,11 @@ def transcoder_put_str(input_ari: str): # PUT /ui/incoming_send/str Body is str ARI to send to transcoder -@router.put("/ui/incoming_send/str", status_code=status.HTTP_200_OK) +@router.put("/ui/incoming_send/str", status_code=status.HTTP_200_OK, + responses={ + status.HTTP_500_INTERNAL_SERVER_ERROR: {"description" : "Error response from NM"}, + status.HTTP_504_GATEWAY_TIMEOUT: {"description" : "Manager response timed out"} + }) async def transcoder_send_ari_str(eid: str, ari: str): try: # Perform translation (API wrapper) From fc68cd2df86a62b87590b6fe924221381ec57817 Mon Sep 17 00:00:00 2001 From: David Edell Date: Tue, 17 Mar 2026 02:01:02 -0400 Subject: [PATCH 31/51] Use HTTP status code enums consistently. --- anms-core/anms/routes/transcoder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index 8e26ac98..bc44d2c0 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -191,13 +191,13 @@ async def transcoder_send_ari_str(eid: str, ari: str): if info.parsed_as != "pending": break if retries <= 0: - raise HTTPException(status_code=504, + raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail={ "idinfo" : idinfo, "info" : info, "status" : "transcoder timeout" }) retries -= 1 if info.parsed_as == "ERROR": - raise HTTPException(status_code=500, + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail={ "idinfo" : idinfo, "info" : info, "status" : 500 }) # Publish From 39852a7a2a35b372cf354b4b72821fd805d6ff51 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 17 Mar 2026 09:57:12 -0400 Subject: [PATCH 32/51] setting cache to store processed reports --- anms-core/anms/routes/ARIs/reports.py | 57 +++++++++++++++++++++------ 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/anms-core/anms/routes/ARIs/reports.py b/anms-core/anms/routes/ARIs/reports.py index 21844ee1..1264c182 100644 --- a/anms-core/anms/routes/ARIs/reports.py +++ b/anms-core/anms/routes/ARIs/reports.py @@ -21,38 +21,65 @@ # the prime contract 80NM0018D0004 between the Caltech and NASA under # subcontract 1658085. # -from typing import List + +# for handling report set and exec set +import ace + import ast +import asyncio + +from cachetools import LFUCache from fastapi import APIRouter, Depends from fastapi import status from fastapi_pagination import Page, Params from fastapi_pagination.ext.async_sqlalchemy import paginate +from fastapi.responses import JSONResponse + +import io + from sqlalchemy import select, and_ from sqlalchemy.engine import Result +from typing import List + from urllib.parse import unquote from anms.components.schemas import ARIs -from anms.models.relational import get_async_session, get_session +from anms.models.relational import get_async_session from anms.models.relational.report import Report from anms.models.relational.execution_set import ExecutionSet from anms.models.relational.registered_agent import RegisteredAgent from anms.shared.opensearch_logger import OpenSearchLogger -import io import anms.routes.transcoder as transcoder -# for handling report set and exec set -import ace + logger = OpenSearchLogger(__name__, log_console=True) router = APIRouter(tags=["REPORTS"]) +# async def _process_reprots(key): + +# return {} + + +# # Cache for Storing translated reports for easy access +# class ReportCache(LFUCache): +# def __missing__(self, key): +# resource = asyncio.create_task( _process_reprots(key)) +# self[key] = resource +# return resource + + +# report_cache = ReportCacheCache(maxsize=16384) + + + # routes for ARIs @router.get("/all", status_code=status.HTTP_200_OK, response_model=Page[ARIs.RptEntry], tags=["REPORTS"]) async def paged_report(params: Params = Depends()): @@ -138,14 +165,13 @@ async def report_def_by_id(agent_id: int): # entries tabulated returns header and values in correct order # handling if nonce_cbor is null +# TODO decomuntate the report definition so the columns are better labeled @router.get("/entries/table/{agent_id}/{nonce_cbor}", status_code=status.HTTP_200_OK) async def report_ac(agent_id: int, nonce_cbor: str) -> dict: ari = None dec = ace.ari_cbor.Decoder() enc = ace.ari_text.Encoder() exec_set_dir = {} - logger.info(nonce_cbor) - logger.info(type(nonce_cbor)) try: store_nonce = nonce_cbor nonce_cbor = ast.literal_eval(nonce_cbor) @@ -153,10 +179,17 @@ async def report_ac(agent_id: int, nonce_cbor: str) -> dict: try: nonce_cbor = ast.literal_eval(str(bytes.fromhex(nonce_cbor))) except Exception as e: - logger.error(f"{e} while processing nonce") - return [] + message = f"{e} while processing nonce:{nonce_cbor}" + logger.error(message) + response = JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"message": message, "error_details": []}) + return response - + if not isinstance(nonce_cbor, (bytes, bytearray)): + message = f"nonce_cbore:{nonce_cbor} should be bytes-like object is required, not 'str'" + logger.error(message) + response = JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"message": message, "error_details": []}) + return response + # process each report in the rpt set and place inside appropiate nonce case or if null use source as key # TODO use td off set in report set to update actual time # @@ -174,10 +207,10 @@ async def report_ac(agent_id: int, nonce_cbor: str) -> dict: try: in_bytes = ace.cborutil.from_hexstr(in_text) ari = dec.decode(io.BytesIO(in_bytes)) - except Exception as err: logger.error(err) - + response = JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"message":err, "error_details": []}) + return response # current ARI should be an report set if ari: if type(ari.value) == ace.ari.ReportSet: From 26799350ccfc1a73f33b8a1e5752762a3d52a47b Mon Sep 17 00:00:00 2001 From: David Edell Date: Tue, 17 Mar 2026 10:48:39 -0400 Subject: [PATCH 33/51] Fixed an additional error return. --- anms-core/anms/routes/transcoder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index bc44d2c0..449a96a6 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -208,6 +208,7 @@ async def transcoder_send_ari_str(eid: str, ari: str): raise e except Exception as e: logger.exception(e) - return status.HTTP_500_INTERNAL_SERVER_ERROR + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + From c26d4690f36c8a1115e0779ea1be2e0f2f12fad5 Mon Sep 17 00:00:00 2001 From: David Linko Date: Fri, 3 Apr 2026 15:18:15 -0400 Subject: [PATCH 34/51] updated to to use newest SQL for reports --- .../anms/components/schemas/ARIs/__init__.py | 1 + .../anms/components/schemas/ARIs/rpt_entry.py | 23 +- .../anms/models/relational/execution_set.py | 3 +- anms-core/anms/models/relational/report.py | 43 ++- anms-core/anms/routes/ARIs/reports.py | 357 +++++++++--------- anms-core/anms/shared/transmogrifier.py | 72 ++-- .../components/management/agents/reports.vue | 71 +--- anms-ui/public/app/shared/api.js | 4 +- anms-ui/public/app/store/modules/agents.js | 1 + anms-ui/server/components/reports.js | 7 +- anms-ui/server/core/routes.js | 2 +- 11 files changed, 290 insertions(+), 294 deletions(-) diff --git a/anms-core/anms/components/schemas/ARIs/__init__.py b/anms-core/anms/components/schemas/ARIs/__init__.py index f4b4932d..d2ceee73 100644 --- a/anms-core/anms/components/schemas/ARIs/__init__.py +++ b/anms-core/anms/components/schemas/ARIs/__init__.py @@ -53,4 +53,5 @@ from .registered_agent import RegisteredAgentInDBBase from .rpt_entry import RptEntry from .rpt_entry import RptEntryName +from .rpt_entry import RptEntryFull from .rpt_entry import RptEntryBaseInDBBase diff --git a/anms-core/anms/components/schemas/ARIs/rpt_entry.py b/anms-core/anms/components/schemas/ARIs/rpt_entry.py index 18812e0a..0263f3e4 100644 --- a/anms-core/anms/components/schemas/ARIs/rpt_entry.py +++ b/anms-core/anms/components/schemas/ARIs/rpt_entry.py @@ -32,12 +32,25 @@ class RptEntryBase(BaseModel): class Config: arbitrary_types_allowed = True - ari_rptset_id: Optional[str] = None - reference_time: Optional[datetime] = None - report_list: Optional[str] = None - agent_id: Optional[int] = None - + ari_rptset_id: Optional[int] = None + reference_time: Optional[datetime] = None + mgr_time: Optional[datetime] = None + nonce_cbor: Optional[str] = None + time_offset: Optional[str] = None + report_source: Optional[str] = None + report_items: Optional[list] = None + +# Shared properties +class RptEntryFull(RptEntryBase): + class Config: + arbitrary_types_allowed = True + orm_mode = True + agent_id: Optional[int] = None + ari_rptlist_id: Optional[int] = None + + + class RptEntryBaseInDBBase(RptEntryBase): class Config: orm_mode = True diff --git a/anms-core/anms/models/relational/execution_set.py b/anms-core/anms/models/relational/execution_set.py index 4aad3698..185134f3 100644 --- a/anms-core/anms/models/relational/execution_set.py +++ b/anms-core/anms/models/relational/execution_set.py @@ -29,6 +29,7 @@ from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import LargeBinary +from anms.shared.transmogrifier import TRANSMORGIFIER # class for vw_ctrl_definition used for build ari @@ -47,7 +48,7 @@ def __repr__(self) -> str: def as_dict(self) -> Dict[str, Any]: dict_obj = { 'execution_set_id': getattr(self, 'execution_set_id'), - 'nonce_cbor': getattr(self, 'nonce_cbor'), + 'nonce_cbor': TRANSMORGIFIER._ace_transcode_just_cbor("0x"+getattr(self, 'nonce_cbor').hex()), 'use_desc': getattr(self, 'use_desc'), 'agent_id': getattr(self, 'agent_id'), 'num_entries': getattr(self, 'num_entries'), diff --git a/anms-core/anms/models/relational/report.py b/anms-core/anms/models/relational/report.py index b66f3f20..173236b5 100644 --- a/anms-core/anms/models/relational/report.py +++ b/anms-core/anms/models/relational/report.py @@ -24,33 +24,48 @@ from typing import Any from typing import Dict +from anms.shared.transmogrifier import TRANSMORGIFIER from anms.models.relational import Model from sqlalchemy import Column from sqlalchemy import Integer -from sqlalchemy import String +from sqlalchemy import DateTime +from sqlalchemy import ARRAY from sqlalchemy import LargeBinary - +from sqlalchemy import orm # class for vw_ctrl_definition used for build ari class Report(Model): - __tablename__ = 'ari_rptset' - ari_rptset_id = Column(Integer, primary_key=True) - nonce_cbor = Column(LargeBinary) - reference_time = Column(Integer) - report_list = Column(String) - report_list_cbor = Column(LargeBinary) - agent_id = Column(Integer) + __tablename__ = 'vw_ari_rpt_set' + ari_rptset_id = Column(Integer, primary_key=True) + mgr_time = Column(DateTime) + reference_time = Column(DateTime) + nonce_cbor = Column(LargeBinary) + agent_id = Column(Integer) + ari_rptset_cbor = Column(LargeBinary) + ari_rptlist_id = Column(Integer) + time_offset = Column(LargeBinary) + report_source = Column(LargeBinary) + report_items = Column(ARRAY(LargeBinary) )#bytea[] NULL + + # processing the raw cbor into an ari object + @orm.reconstructor + def init_on_load(self): + self.nonce_cbor = TRANSMORGIFIER.transcode("0x"+getattr(self, 'nonce_cbor').hex())['uri'] + self.time_offset = TRANSMORGIFIER.transcode("0x"+getattr(self, 'time_offset').hex())['uri'] + self.report_source = TRANSMORGIFIER.transcode("0x"+getattr(self, 'report_source').hex())['uri'] + self.report_items = [TRANSMORGIFIER.transcode("0x"+x.hex())['uri'] for x in getattr(self, 'report_items')] + def __repr__(self) -> str: return self.as_dict().__repr__() def as_dict(self) -> Dict[str, Any]: dict_obj = { 'ari_rptset_id': getattr(self, 'ari_rptset_id'), - 'nonce_cbor': getattr(self, 'nonce_cbor'), 'reference_time': getattr(self, 'reference_time'), - 'report_list': getattr(self, 'report_list'), - 'report_list_cbor': getattr(self, 'report_list_cbor'), - 'agent_id': getattr(self, 'agent_id') + 'nonce_cbor': getattr(self, 'nonce_cbor'), + 'agent_id': getattr(self, 'agent_id'), + 'ari_rptlist_id': getattr(self, 'ari_rptlist_id'), + 'report_source': getattr(self, 'report_source'), + 'report_items': getattr(self, 'report_items') } - return dict_obj diff --git a/anms-core/anms/routes/ARIs/reports.py b/anms-core/anms/routes/ARIs/reports.py index 1264c182..d71c5edf 100644 --- a/anms-core/anms/routes/ARIs/reports.py +++ b/anms-core/anms/routes/ARIs/reports.py @@ -35,6 +35,7 @@ from fastapi_pagination import Page, Params from fastapi_pagination.ext.async_sqlalchemy import paginate from fastapi.responses import JSONResponse +from anms.shared.transmogrifier import TRANSMORGIFIER import io @@ -49,210 +50,202 @@ from anms.models.relational import get_async_session from anms.models.relational.report import Report -from anms.models.relational.execution_set import ExecutionSet +from anms.models.relational.const import Const from anms.models.relational.registered_agent import RegisteredAgent from anms.shared.opensearch_logger import OpenSearchLogger -import anms.routes.transcoder as transcoder - - +from datetime import datetime logger = OpenSearchLogger(__name__, log_console=True) router = APIRouter(tags=["REPORTS"]) - -# async def _process_reprots(key): - -# return {} - - -# # Cache for Storing translated reports for easy access -# class ReportCache(LFUCache): -# def __missing__(self, key): -# resource = asyncio.create_task( _process_reprots(key)) -# self[key] = resource -# return resource - - -# report_cache = ReportCacheCache(maxsize=16384) - - - + # routes for ARIs -@router.get("/all", status_code=status.HTTP_200_OK, response_model=Page[ARIs.RptEntry], tags=["REPORTS"]) -async def paged_report(params: Params = Depends()): +@router.get("/page", status_code=status.HTTP_200_OK, response_model=Page[ARIs.RptEntryFull], tags=["REPORTS"]) +async def paged_reports(params: Params = Depends()): async with get_async_session() as session: return await paginate(session, select(Report), params) - -@router.get("/name/all", status_code=status.HTTP_200_OK, response_model=List[ARIs.RptEntryBaseInDBBase], tags=["REPORTS"]) -async def all_report_name(): +@router.get("/all", status_code=status.HTTP_200_OK,response_model=List[ARIs.RptEntryFull], tags=["REPORTS"]) +async def all_reports(): stmt = select(Report) + res = [] async with get_async_session() as session: result: Result = await session.scalars(stmt) - return result.all() + res = result.all() + return res + + +# report_source is cbor +async def _report_from_id_source(agent_idx: int, report_source: str, start_time: str = None, end_time: str = None): + res = [] + report_dict =[] + + + if(agent_idx): + if start_time is None: + start_time = datetime.fromisoformat("2010-01-01T00:00:00+00:00") + if end_time is None: + end_time = datetime.fromisoformat("2100-01-01T00:00:00+00:00") + start_time = start_time.replace(tzinfo=None) + end_time = end_time.replace(tzinfo=None) + + stmt = select(Report).where(Report.agent_id == agent_idx).where(Report.report_source == bytes.fromhex(report_source)).filter(Report.reference_time >= start_time).filter(Report.reference_time <= end_time) + async with get_async_session() as session: + result: Result = await session.scalars(stmt) + res = result.all() + + if(res): + # translate report_source if its const use its values as the forms for the final report + report_source_ari = TRANSMORGIFIER.transcode("0x"+report_source) + report_source_columns=[f"col {x}" for x,_ in enumerate(res[0].report_items)] + if(isinstance(report_source_ari["ari"], ace.ari.ReferenceARI)): + if(report_source_ari["ari"].ident.type_id == ace.ari.StructType.CONST): + stmt = select(Const.data_value).where(Const.name == str(report_source_ari["ari"].ident.obj_id) ).where(Const.data_model_name == str(report_source_ari["ari"].ident.model_id)).where(Const.namespace == str(report_source_ari["ari"].ident.org_id)) + async with get_async_session() as session: + result: Result = await session.scalars(stmt) + result = result.all() + if result: + report_source_columns = [] + for val in TRANSMORGIFIER.transcode(result[0])["ari"].value: + report_source_columns.append(val.ident.obj_id) + else: + if(isinstance(report_source_ari["ari"].ident, ace.ari.LiteralARI)): + if(isinstance(report_source_ari["ari"].ident.value,list)): + report_source_columns = [] + for val in report_source_ari.value: + report_source_columns.append(val.ident.obj_id) + + + # data_value + for row in res: + new_item = {"reference_time":row.reference_time, + "mgr_time":row.mgr_time, + "time_offset":row.time_offset, + "rpt_set_nonce":row.nonce_cbor, + "report_source":report_source_ari['uri']} + + for index, value in enumerate(row.report_items): + new_item[report_source_columns[index]] = value + report_dict.append(new_item) + + return report_dict + + +async def _source_from_id(agent_idx: int): + res = [] + if(agent_idx): + stmt = select(Report.report_source).distinct().where(Report.agent_id == agent_idx) + async with get_async_session() as session: + result: Result = await session.scalars(stmt) + for x in result.all(): + res.append({"ari":TRANSMORGIFIER.transcode("0x"+x.hex())['uri'], "cbor":x.hex()}) + return res + + +async def _reports_from_id(agent_idx: int): + res = [] + if(agent_idx): + stmt = select(Report).where(Report.agent_id == agent_idx) + async with get_async_session() as session: + result: Result = await session.scalars(stmt) + res = result.all() + return res + +@router.get("/all/eid/{agent_eid}", status_code=status.HTTP_200_OK, response_model=List[ARIs.RptEntry], + tags=["REPORTS"]) +async def reports_agent_by_name(agent_eid: str): + agent_idx = None + agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) + async with get_async_session() as session: + # Execution set uses URI as agent_id + result_agent: Result = await session.scalars(agent_id_stmt) + agent_idx = result_agent.one_or_none() + if(agent_idx): + agent_idx = agent_idx.registered_agents_id + + return await _reports_from_id(agent_idx) + + +@router.get("/all/idx/{agent_idx}", status_code=status.HTTP_200_OK, response_model=List[ARIs.RptEntry], + tags=["REPORTS"]) +async def reports_agent_by_id(agent_idx: int): + return await _reports_from_id(agent_idx) -@router.get("/entry/name/{agent_id}", status_code=status.HTTP_200_OK, response_model=list, +@router.get("/report_source/eid/{agent_eid}", status_code=status.HTTP_200_OK, response_model=list, tags=["REPORTS"]) -async def report_def_by_id(agent_id: int): - # select all reports belonging to the agent - final_res = [] - agent_id_str = "" - dec = ace.ari_cbor.Decoder() - enc = ace.ari_text.Encoder() - adms = ace.AdmSet() - adms.load_default_dirs() - nn_func = ace.nickname.Converter(ace.nickname.Mode.FROM_NN , adms.db_session(), False) - stmt = select(Report).where(Report.agent_id == agent_id) - agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.registered_agents_id == agent_id) +async def reports_source_agent_by_name(agent_eid: str): + agent_idx = None + agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) async with get_async_session() as session: - result: Result = await session.scalars(stmt) # Execution set uses URI as agent_id result_agent: Result = await session.scalars(agent_id_stmt) - agent_id_str = result_agent.one_or_none() - agent_id_str = agent_id_str.agent_endpoint_uri - for res in result.all(): - # select from exec_set - try: - nonce_cbor = res.nonce_cbor - if(nonce_cbor != b'\xf6'): # not a null nonce - stmt = select(ExecutionSet).where(and_(ExecutionSet.agent_id == agent_id_str, ExecutionSet.nonce_cbor == nonce_cbor) ) - result: Result = await session.scalars(stmt) - exc_set = result.all() - for res_exec in exc_set: - ari_val = "" - if(res_exec): - hex_str = res_exec.entries.hex() - hex_str = "0x"+hex_str.upper() - ari_val = await transcoder.transcoder_put_cbor_await(hex_str) - ari_val = ari_val['data'] - addition = {'exec_set': ari_val,'nonce_cbor':str(nonce_cbor)} - if addition not in final_res: - final_res.append(addition) - else: #null nonce use report source - rpt_set = res.report_list_cbor.hex() - # Using Ace to translate CBOR into ARI object to process individual parts - in_text = '0x'+rpt_set - ari_rpt = None - try: - in_bytes = ace.cborutil.from_hexstr(in_text) - ari_rpt = dec.decode(io.BytesIO(in_bytes)) - except Exception as err: - logger.error(err) - - # running through and translating all parts of rptset - for rpt in ari_rpt.value.reports: - try: - enc = ace.ari_text.Encoder() - buf = io.StringIO() - enc.encode(rpt.source, buf) - out_text = buf.getvalue() - ari_val = out_text - # TODO look at better way to handle storing nonce with null - addition = {'exec_set': ari_val,'nonce_cbor':str(nonce_cbor)} - if addition not in final_res: - final_res.append(addition) - except Exception as err: - logger.error(err) - - except Exception as e: - logger.error(f"Error {e}, while processing nonce:{nonce_cbor} for agent: {agent_id_str}") - - return final_res - -# entries tabulated returns header and values in correct order -# handling if nonce_cbor is null -# TODO decomuntate the report definition so the columns are better labeled -@router.get("/entries/table/{agent_id}/{nonce_cbor}", status_code=status.HTTP_200_OK) -async def report_ac(agent_id: int, nonce_cbor: str) -> dict: - ari = None - dec = ace.ari_cbor.Decoder() - enc = ace.ari_text.Encoder() - exec_set_dir = {} - try: - store_nonce = nonce_cbor - nonce_cbor = ast.literal_eval(nonce_cbor) - except Exception as e: - try: - nonce_cbor = ast.literal_eval(str(bytes.fromhex(nonce_cbor))) - except Exception as e: - message = f"{e} while processing nonce:{nonce_cbor}" - logger.error(message) - response = JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"message": message, "error_details": []}) - return response - - if not isinstance(nonce_cbor, (bytes, bytearray)): - message = f"nonce_cbore:{nonce_cbor} should be bytes-like object is required, not 'str'" - logger.error(message) - response = JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"message": message, "error_details": []}) - return response + agent_idx = result_agent.one_or_none() + if(agent_idx): + agent_idx = agent_idx.registered_agents_id - # process each report in the rpt set and place inside appropiate nonce case or if null use source as key - # TODO use td off set in report set to update actual time - # - ari = None - stmt = select(Report).where(and_(Report.agent_id == agent_id, Report.nonce_cbor == nonce_cbor) ) + reports = await _source_from_id(agent_idx) + return reports + + +@router.get("/report_source/idx/{agent_idx}/", status_code=status.HTTP_200_OK, response_model=list, + tags=["REPORTS"]) +async def reports_source_agent_by_id(agent_idx: int): + reports = await _source_from_id(agent_idx) + return reports + + +@router.get("/dictionary/idx/{agent_idx}/{source_cbor}", status_code=status.HTTP_200_OK, response_model=list, + tags=["REPORTS"]) +async def reports_dictionary_by_id_and_report_source(agent_idx: int, source_cbor: str): + reports = await _report_from_id_source(agent_idx, source_cbor) + return reports + +@router.get("/dictionary/eid/{agent_eid}/{source_cbor}", status_code=status.HTTP_200_OK, response_model=list, + tags=["REPORTS"]) +async def reports_dictionary_by_name_and_report_source(agent_eid: str, source_cbor: str): + agent_idx = None + agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) async with get_async_session() as session: - result: Result = await session.scalars(stmt) - for res in result.all(): - # used to hold final report set - curr_time = res.reference_time - # addition = {time:} - rpt_set = res.report_list_cbor.hex() - # Using Ace to translate CBOR into ARI object to process individual parts - in_text = '0x'+rpt_set - try: - in_bytes = ace.cborutil.from_hexstr(in_text) - ari = dec.decode(io.BytesIO(in_bytes)) - except Exception as err: - logger.error(err) - response = JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"message":err, "error_details": []}) - return response - # current ARI should be an report set - if ari: - if type(ari.value) == ace.ari.ReportSet: - # for each report in a rptset - # add to the top level nonce dict or to source dict if nonce is null null - for rpt in ari.value.reports: - try: - # structure for the reports - # time: source_name:{[values of reprots ]} - buf = io.StringIO() - enc.encode(rpt.source, buf) - rpt_src = buf.getvalue() - addition = {"time":curr_time, rpt_src:[]} - rpt_entries = [] - enc = ace.ari_text.Encoder() - # running through and translating all parts of rptset - for item in rpt.items: - # using ace to decode the components - # item = dec.decode(item) - if type(item.value) == ace.ari.Table: - table_vals = [] - for tab_val in item.value: - table_vals.append([t.value for t in tab_val]) - rpt_entries.append(table_vals) - else:#handle values as normal - buf = io.StringIO() - enc.encode(item, buf) - out_text = buf.getvalue() - rpt_entries.append(out_text) - - # placing all the values in the sources section - addition[rpt_src] = rpt_entries - - if(nonce_cbor == b'\xf6' ): - curr_dic = exec_set_dir.get(rpt_src,[]) - curr_dic.append(addition) - exec_set_dir[rpt_src] = curr_dic - else: - curr_dic = exec_set_dir.get(store_nonce,[]) - curr_dic.append(addition) - exec_set_dir[store_nonce] = curr_dic - except Exception as err: - logger.error(err) - return exec_set_dir + # Execution set uses URI as agent_id + result_agent: Result = await session.scalars(agent_id_stmt) + agent_idx = result_agent.one_or_none() + if(agent_idx): + agent_idx = agent_idx.registered_agents_id + + reports = await _report_from_id_source(agent_idx, source_cbor) + return reports + + + +# using the known search criteria to filter the reports +@router.post("/dictionary/search/idx/", status_code=status.HTTP_200_OK, response_model=list, + tags=["REPORTS"]) +async def reports_dictionary_by_search_idx(agent_idxs: list[int], source_cbors: list, start_time: datetime=None, end_time: datetime=None): + reports = [] + for agent_idx in agent_idxs: + for source_cbor in source_cbors: + reports.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) + + return reports + +@router.post("/dictionary/search/eid/", status_code=status.HTTP_200_OK, response_model=list, + tags=["REPORTS"]) +async def reports_dictionary_by_search_eid(agent_eids: list[str], source_cbors: list, start_time: datetime=None, end_time: datetime=None): + reports = [] + for agent_eid in agent_eids: + agent_idx = None + agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) + async with get_async_session() as session: + # Execution set uses URI as agent_id + result_agent: Result = await session.scalars(agent_id_stmt) + agent_idx = result_agent.one_or_none() + if(agent_idx): + agent_idx = agent_idx.registered_agents_id + for source_cbor in source_cbors: + reports.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) + return reports diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index 94267bc4..60210b3e 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -59,12 +59,14 @@ def __init__(self, args): LOGGER.info(f'Connecting to SQL DB at {db_uri}') self._dbeng = sqlalchemy.create_engine(db_uri) self.transcode = self._transcode_internal + self.transcode_direct = self._transcode_internal self.reload = self._reload_internal self._adm_reload(None) else: # setting up the MQTT server instead self.MQTT_CLIENT = anms.shared.mqtt_client.MQTT_CLIENT self.transcode = self._transcode_mqtt + self.transcode_direct = self._transcode_mqtt_direct self.reload = self._reload_mqtt async def handle_adm(self, admset: ace.AdmSet, adm_file: ace.models.AdmModule, session, replace=True): @@ -153,17 +155,31 @@ def _transcode_mqtt(self, input): LOGGER.info(f'PUBLISH to transcode/CoreFacing/Outgoing, msg = {msg}') self.MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) + def _transcode_mqtt_direct(self, input): + msg = json.dumps({'uri': input}) + LOGGER.info(f'PUBLISH to transcode/CoreFacing/Outgoing, msg = {msg}') + self.MQTT_CLIENT.publish("transcode/CoreFacing/Outgoing", msg) + return "pending" + def _transcode_internal(self, input): - self._ace_transcode(input) - - # picking up any stray items that didnt get translated - pending_uris = TranscoderLog.query.filter_by(parsed_as='pending').all() - for entrys in pending_uris: - try: - self._ace_transcode(entrys.input_string) - except Exception as err: - LOGGER.error('Failed to process pending entry: %s', err) + LOGGER.info(f"translating {input}") + ari = self._ace_transcode(input) + return ari + + def _ace_transcode_just_cbor(self, input): + adms = ace.AdmSet() + dec = ace.ari_cbor.Decoder() + + in_text = input.strip() + in_bytes = ace.cborutil.from_hexstr(in_text) + ari = dec.decode(io.BytesIO(in_bytes)) + enc = ace.ari_text.Encoder() + buf = io.StringIO() + enc.encode(ari, buf) + out_text = buf.getvalue() + return out_text + def _ace_transcode(self, input): # result object to fill in res_obj = {} @@ -185,14 +201,15 @@ def _ace_transcode(self, input): try: in_bytes = ace.cborutil.from_hexstr(in_text) dec = ace.ari_cbor.Decoder() - ari = dec.decode(io.BytesIO(in_bytes)) - LOGGER.debug(f'decoded as ARI {ari}') - ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari) - + ari_no_nn = dec.decode(io.BytesIO(in_bytes)) + LOGGER.debug(f'decoded as ARI {ari_no_nn}') + ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari_no_nn) except Exception as err: - raise RuntimeError(f"Error decoding from `{in_text}`: {err}") from err + LOGGER.error(f"Error decoding from `{in_text}`: {err}") + ari = ari_no_nn + res_obj['cbor'] = in_text - res_obj['ari'] = f"{ari}" + res_obj['ari'] = ari try: enc = ace.ari_text.Encoder() @@ -204,7 +221,8 @@ def _ace_transcode(self, input): out_text = 'ari:' + out_text LOGGER.debug(f'encoded as text {out_text}') except Exception as err: - raise RuntimeError(f"Error encoding from {ari}: {err}") from err + LOGGER.error(f"Error encoding from {ari}: {err}") + res_obj['uri'] = out_text else: @@ -213,11 +231,12 @@ def _ace_transcode(self, input): try: dec = ace.ari_text.Decoder() - ari = dec.decode(io.StringIO(in_text)) - LOGGER.debug(f'decoded as ARI {ari}') - ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari) + ari_no_nn = dec.decode(io.StringIO(in_text)) + LOGGER.debug(f'decoded as ARI {ari_no_nn}') + ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari_no_nn) except Exception as err: - raise RuntimeError(f"Error decoding from `{in_text}`: {err}") from err + LOGGER.error(f"Error decoding from `{in_text}`: {err}") + ari = ari_no_nn # rencoding ari to ensure using non nicknames try: @@ -230,10 +249,11 @@ def _ace_transcode(self, input): out_text = 'ari:' + out_text LOGGER.debug(f'encoded as text {out_text}') except Exception as err: - raise RuntimeError(f"Error encoding from {ari}: {err}") from err + LOGGER.error(f"Error encoding from {ari}: {err}") + res_obj['uri'] = out_text - res_obj['ari'] = f"{ari}" + res_obj['ari'] = ari try: enc = ace.ari_cbor.Encoder() @@ -243,7 +263,8 @@ def _ace_transcode(self, input): hex_str = ace.cborutil.to_hexstr(buf.getvalue()) LOGGER.info(f'encoded as binary {hex_str}') except Exception as err: - raise RuntimeError(f"Error encoding from {ari}: {err}") from err + LOGGER.error(f"Error encoding from {ari}: {err}") + res_obj['cbor'] = hex_str except Exception as err: res_obj['ari'] = f'Failed to process: {err}' @@ -255,15 +276,14 @@ def _ace_transcode(self, input): with get_session() as session: session.query(TranscoderLog).filter(TranscoderLog.input_string == input).update({ 'parsed_as': res_obj['parsedAs'], - 'ari': json.dumps(res_obj['ari']), + 'ari': json.dumps(f"{res_obj['ari']}"), 'cbor': res_obj['cbor'], 'uri': res_obj['uri'] }) session.commit() LOGGER.info(f'Response {res_obj}') - # client.publish('transcode/CodexFacing/Outgoing', json.dumps(res_obj)) - # just log it back into the database + return res_obj diff --git a/anms-ui/public/app/components/management/agents/reports.vue b/anms-ui/public/app/components/management/agents/reports.vue index 34f10126..96054184 100644 --- a/anms-ui/public/app/components/management/agents/reports.vue +++ b/anms-ui/public/app/components/management/agents/reports.vue @@ -8,21 +8,10 @@ -
    - -
    - -
    -
    + @@ -44,8 +33,11 @@ export default { tableHeaders: [], tableItems: [], title: "", - reports: {}, - reportsHeader: {}, + reports: [], + reportsHeader: [{ key: 'reference_time', label: 'Time', sortable: true }, + { key: 'mgr_time', label: 'mgr_time', sortable: true }, + { key: 'rpt_set_nonce', label: 'rpt_set_nonce' } + ], loading: true, } }, @@ -55,13 +47,12 @@ export default { this.tableHeaders = []; this.tableItems = []; this.loading = true; - let nonce_cbor = this.selected.nonce_cbor; - - await api.methods.apiEntriesForReport(this.registered_agents_id,encodeURIComponent(nonce_cbor)) + let report_source = this.selected.cbor; + this.reports= []; + await api.methods.apiEntriesForReport(this.registered_agents_id,report_source) .then(res => { - this.processReport(res.data); - this.reports[this.selected] = this.tableItems; - this.reportsHeader[this.selected] = this.tableHeaders; + + this.reports = res.data; }).catch(error => { // handle error console.error("reports error", error); @@ -70,49 +61,11 @@ export default { this.loading = false; }, - processReport(report) { - let rpt = []; - if(this.selected.exec_set in report){ - rpt = report[this.selected.exec_set]; - }else{ - rpt = report[this.selected.nonce_cbor]; - } - - let currTableItems = []; - // let holdHeader = rpt.shift(); - - console.log(rpt); - - let holdHeader = Object.keys(rpt[0]); - console.log(holdHeader); - for (let item of rpt) { - let row = []; - for (let hI of holdHeader) { - row.push(item[hI]); - } - currTableItems.push(row.flat()); - console.log(currTableItems); - } - - this.tableHeaders.push([holdHeader]); - this.tableItems.push(currTableItems); - } }, computed: { }, mounted() { this.loading = true; - this.rptts.forEach((rpt, index) => { - api.methods.apiEntriesForReport(this.registered_agents_id, rpt.nonce_cbor) - .then(res => { - this.reports[index] = res.data - }).catch(error => { - // handle error - console.error("reports error", error); - console.log("error obj:", error); - - }); - }); this.loading = false; }, } diff --git a/anms-ui/public/app/shared/api.js b/anms-ui/public/app/shared/api.js index 0abab77d..e3d2c31d 100644 --- a/anms-ui/public/app/shared/api.js +++ b/anms-ui/public/app/shared/api.js @@ -154,8 +154,8 @@ export default { return axios.get(Constants.BASE_API_URL+'nm/agents', nodeEID) }, - apiEntriesForReport(obj_agent_id, correlator_nonce) { - return axios.get(Constants.BASE_API_URL+`report/entries/table/${obj_agent_id}/${correlator_nonce}`) + apiEntriesForReport(obj_agent_id, source_cbor) { + return axios.get(Constants.BASE_API_URL+`report/entries/table/${obj_agent_id}/${source_cbor}`) }, apiEntriesForReportTemplate(agentId){ diff --git a/anms-ui/public/app/store/modules/agents.js b/anms-ui/public/app/store/modules/agents.js index 07c0d99a..d9c05262 100644 --- a/anms-ui/public/app/store/modules/agents.js +++ b/anms-ui/public/app/store/modules/agents.js @@ -96,6 +96,7 @@ export default { api.methods.apiEntriesForReportTemplate(res.data.registered_agents_id) .then(res => { commit('rptt', res.data) + console.log(res.data) }).catch(error => { // handle error console.error("get agent rptt error", error); diff --git a/anms-ui/server/components/reports.js b/anms-ui/server/components/reports.js index c0c3e780..0fe5da7b 100644 --- a/anms-ui/server/components/reports.js +++ b/anms-ui/server/components/reports.js @@ -34,7 +34,7 @@ // /entry/name/{agent_id} let obj_agent_id = req.params.obj_agent_id - const url = utils.generateAnmsCoreUrl(['report','entry','name', obj_agent_id]); + const url = utils.generateAnmsCoreUrl(['report','report_source','idx', obj_agent_id]); const aris = await axios.get(url); return res.status(200).json(aris.data); } catch (err) { @@ -44,11 +44,10 @@ exports.getReportEntriesByAgent = async function(req,res,next){ try { - // /entry/values/{agent_id}/{ADM}/{report_name} let obj_agent_id = req.params.obj_agent_id - let correlator_nonce = encodeURIComponent(req.params.correlator_nonce) + let source_cbor = req.params.source_cbor - const url = utils.generateAnmsCoreUrl(['report','entries','table', obj_agent_id, correlator_nonce]); + const url = utils.generateAnmsCoreUrl(['report','dictionary','idx', obj_agent_id, source_cbor]); const name_entries = await axios.get(url); return res.status(200).json(name_entries.data); } catch (err) { diff --git a/anms-ui/server/core/routes.js b/anms-ui/server/core/routes.js index fb9a1b7f..fc872a44 100755 --- a/anms-ui/server/core/routes.js +++ b/anms-ui/server/core/routes.js @@ -150,7 +150,7 @@ // --Reports Routes -- // const reports = require('../components/reports') router.get('/report/entry/name/:obj_agent_id', reports.getReportNameByAgent); - router.get('/report/entries/table/:obj_agent_id/:correlator_nonce', reports.getReportEntriesByAgent); + router.get('/report/entries/table/:obj_agent_id/:source_cbor', reports.getReportEntriesByAgent); //------------- Unknown API Routes -------------// router.all('/*', function (req, res, next) { From 3fc2e5cc60086b51cfc892f908cc79e333f3c7d1 Mon Sep 17 00:00:00 2001 From: David Linko Date: Fri, 3 Apr 2026 15:18:43 -0400 Subject: [PATCH 35/51] added COPY for startup.uri --- testenv.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/testenv.Containerfile b/testenv.Containerfile index 2b861e24..4ba6ad68 100644 --- a/testenv.Containerfile +++ b/testenv.Containerfile @@ -129,6 +129,7 @@ RUN dnf -y install container-tools RUN systemctl disable dnf-makecache.timer COPY --chmod=755 deps/dtnma-tools/systemd/service_is_running.sh /usr/local/bin/service_is_running +COPY deps/dtnma-tools/integration-test-ion/startup.uri /etc/refda/startup.uri # Image for the test environment manager transport with ION node and the # ion-app-proxy daemon From 9a302113686230aad692a376fbfa772436e674c5 Mon Sep 17 00:00:00 2001 From: David Linko Date: Fri, 3 Apr 2026 15:19:27 -0400 Subject: [PATCH 36/51] added for querying for report templates --- anms-core/anms/models/relational/const.py | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 anms-core/anms/models/relational/const.py diff --git a/anms-core/anms/models/relational/const.py b/anms-core/anms/models/relational/const.py new file mode 100644 index 00000000..60265cb0 --- /dev/null +++ b/anms-core/anms/models/relational/const.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 The Johns Hopkins University Applied Physics +# Laboratory LLC. +# +# This file is part of the Asynchronous Network Management System (ANMS). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This work was performed for the Jet Propulsion Laboratory, California +# Institute of Technology, sponsored by the United States Government under +# the prime contract 80NM0018D0004 between the Caltech and NASA under +# subcontract 1658085. +# +from typing import Any +from typing import Dict + +from anms.models.relational import Model +from sqlalchemy import Column +from sqlalchemy import Integer +from sqlalchemy import String + +# class for vw_ctrl_definition used for build ari +class Const(Model): + __tablename__ = 'vw_const_actual' + obj_actual_definition_id = Column(Integer, primary_key=True) + data_type = Column(String) + data_value = Column(String) + use_desc = Column(String) + obj_metadata_id = Column(Integer) + data_model_name = Column(String) + namespace = Column(String) + data_type_id = Column(Integer) + name = Column(String) + data_model_id = Column(Integer) + object_enumeration = Column(Integer) + status = Column(String) + reference = Column(String) + description = Column(String) + + def __repr__(self) -> str: + return self.as_dict().__repr__() + + def as_dict(self) -> Dict[str, Any]: + dict_obj = { + c.name: getattr(self, c.name) for c in self.__table__.columns + } + + return dict_obj From 525fbcf9e61ac3baf0e8c337d0eba6b1e54f401c Mon Sep 17 00:00:00 2001 From: David Linko Date: Mon, 6 Apr 2026 18:44:57 -0400 Subject: [PATCH 37/51] updated grafana to use new rptsets --- anms-core/anms/routes/ARIs/reports.py | 12 +- anms-core/anms/routes/transcoder.py | 2 +- .../provisioning/dashboards/anms-monitor.json | 138 ++++++++++-------- .../provisioning/datasources/datasource.yml | 4 +- 4 files changed, 87 insertions(+), 69 deletions(-) diff --git a/anms-core/anms/routes/ARIs/reports.py b/anms-core/anms/routes/ARIs/reports.py index d71c5edf..f6eab289 100644 --- a/anms-core/anms/routes/ARIs/reports.py +++ b/anms-core/anms/routes/ARIs/reports.py @@ -227,16 +227,18 @@ async def reports_dictionary_by_name_and_report_source(agent_eid: str, source_cb async def reports_dictionary_by_search_idx(agent_idxs: list[int], source_cbors: list, start_time: datetime=None, end_time: datetime=None): reports = [] for agent_idx in agent_idxs: + rpt_cur = [] for source_cbor in source_cbors: - reports.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) - + rpt_cur.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) + reports.append({"agent": agent_idx, "reports": rpt_cur} ) return reports @router.post("/dictionary/search/eid/", status_code=status.HTTP_200_OK, response_model=list, tags=["REPORTS"]) async def reports_dictionary_by_search_eid(agent_eids: list[str], source_cbors: list, start_time: datetime=None, end_time: datetime=None): - reports = [] + reports = [] for agent_eid in agent_eids: + rpt_cur = [] agent_idx = None agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) async with get_async_session() as session: @@ -246,6 +248,6 @@ async def reports_dictionary_by_search_eid(agent_eids: list[str], source_cbors: if(agent_idx): agent_idx = agent_idx.registered_agents_id for source_cbor in source_cbors: - reports.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) - + rpt_cur.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) + reports.append({"agent": agent_eid, "reports":rpt_cur} ) return reports diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index 449a96a6..cd6ca8fb 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -143,7 +143,7 @@ async def transcoder_put_await_str(input_ari: str): def transcoder_incoming_str(input_ari: str): return _transcoder_put_str(input_ari) -def transcoder_put_str(input_ari: str): +def _transcoder_put_str(input_ari: str): input_ari = input_ari.strip() transcoder_log_id = None send_to_transcode = False diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json index e3a1433d..26c5b327 100644 --- a/grafana/provisioning/dashboards/anms-monitor.json +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -24,7 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 1, + "id": 0, "links": [], "panels": [ { @@ -117,11 +117,12 @@ "type": "postgres", "uid": "amp_core" }, + "editorMode": "code", "format": "time_series", "group": [], "metricColumn": "none", "rawQuery": true, - "rawSql": "SELECT\n $__timeGroup(reference_time::timestamp,'1m') as time,\n count(report_list) as value,\n agent_endpoint_uri AS metric\nFROM ari_rptset join registered_agents on ari_rptset.agent_id = registered_agents.registered_agents_id\ngroup by 1, agent_endpoint_uri\norder by time asc", + "rawSql": "SELECT\n $__timeGroup(reference_time::timestamp,'1m') as time,\n count(report_items) as value,\n agent_endpoint_uri AS metric\nFROM vw_ari_rpt_set join registered_agents on vw_ari_rpt_set.agent_id = registered_agents.registered_agents_id\ngroup by 1, agent_endpoint_uri\norder by time asc", "refId": "A", "select": [ [ @@ -133,6 +134,23 @@ } ] ], + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + }, "timeColumn": "time", "where": [ { @@ -458,11 +476,12 @@ "type": "postgres", "uid": "amp_core" }, + "editorMode": "code", "format": "table", "group": [], "metricColumn": "none", "rawQuery": true, - "rawSql": "-- all reports \nSELECT \n reference_time,\n agent_id, \n report_list\nFROM\n ari_rptset", + "rawSql": "-- all reports \nSELECT \n reference_time,\n mgr_time,\n agent_id, \n encode(time_offset,'hex') as time_offset,\n encode(report_source,'hex') as report_source,\n report_items\nFROM\n vw_ari_rpt_set", "refId": "A", "select": [ [ @@ -474,6 +493,23 @@ } ] ], + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + }, "timeColumn": "time", "where": [ { @@ -574,8 +610,7 @@ }, { "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" + "uid": "P1671BF01EA0D6F15" }, "fieldConfig": { "defaults": { @@ -583,7 +618,7 @@ "mode": "palette-classic" }, "custom": { - "axisBorderShow": false, + "axisBorderShow": true, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -601,12 +636,12 @@ "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 1, - "pointSize": 5, + "pointSize": 3, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", - "showValues": false, + "showValues": true, "spanNulls": false, "stacking": { "group": "A", @@ -616,6 +651,7 @@ "mode": "off" } }, + "fieldMinMax": true, "mappings": [], "thresholds": { "mode": "absolute", @@ -642,10 +678,15 @@ "id": 74, "options": { "legend": { - "calcs": [], + "calcs": [ + "lastNotNull", + "max" + ], "displayMode": "table", "placement": "right", - "showLegend": true + "showLegend": true, + "sortBy": "First *", + "sortDesc": false }, "tooltip": { "hideZeros": false, @@ -657,55 +698,40 @@ "targets": [ { "columns": [], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" - }, + "computed_columns": [], "filters": [], "format": "table", "global_query_id": "", "parser": "backend", "refId": "A", - "root_selector": "", + "root_selector": "$map(reports, function($v) {\n $v.nestedArray.{\n \"value\": \"col 0\"\n }\n})\n", "source": "url", "type": "uql", - "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_3_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_3_6\"\n ", - "url": "1/b%27%5Cx18%7B%27", + "uql": "parse-json\n| mv-expand \"reports\"\n| mv-expand \"reports\"\n| extend \"reference_time\"=todatetime(\"reports.reference_time\")\n| extend \"num_msg_rx\"=\"reports.col 0\"\n| extend \"parts\"=split(\"num_msg_rx\",'/')\n| extend \"parts\"=\"parts[2]\"\n| extend \"num_msg_rx\"=toint(\"parts\") \n| extend \"time\"=\"reference_time\"\n| extend \"value\"=\"num_msg_rx\"\n| extend \"metric\"=\"agent\"\n| project \"time\", \"value\", \"agent\"\n\n\n\n\n\n", + "url": "report/dictionary/search/eid/", "url_options": { - "data": "", - "method": "GET" + "body_content_type": "application/json", + "body_type": "raw", + "data": "{\n \"agent_eids\": [\n \"ipn:2.6\",\"ipn:3.6\"\n ],\n \"source_cbors\": [\n \"8564696574666b64746e6d612d6167656e742267696e7370656374818464696574666b64746e6d612d6167656e74236a6e756d2d6d73672d7278\"\n ]\n}", + "method": "POST" } - }, + } + ], + "title": "ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx)", + "transformations": [ { - "columns": [], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" - }, - "filters": [], - "format": "table", - "global_query_id": "", - "hide": false, - "parser": "backend", - "refId": "B", - "root_selector": "", - "source": "url", - "type": "uql", - "uql": "parse-json\n | scope \"b'\\\\x18{'\"\n | extend \"num_msg_rx\"=\"ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx).0\"\n | extend \"num_msg_rx\"=split(\"num_msg_rx\",'/')\n | extend \"num_msg_rx\"=\"num_msg_rx.2\"\n | extend \"ipn_2_6\"=tonumber(\"num_msg_rx\")\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"ipn_2_6\"\n ", - "url": "2/b%27%5Cx18%7B%27", - "url_options": { - "data": "", - "method": "GET" + "id": "prepareTimeSeries", + "options": { + "format": "multi" } } ], - "title": "ari://ietf/dtnma-agent/CTRL/inspect(//ietf/dtnma-agent/EDD/num-msg-rx)", "type": "timeseries" }, { "datasource": { "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" + "uid": "P1671BF01EA0D6F15" }, "fieldConfig": { "defaults": { @@ -720,9 +746,6 @@ "footer": { "reducers": [] }, - "hideFrom": { - "viz": false - }, "inspect": false }, "mappings": [], @@ -760,32 +783,25 @@ "columns": [], "datasource": { "type": "yesoreyeram-infinity-datasource", - "uid": "PA0935669E4DE7FB6" + "uid": "P1671BF01EA0D6F15" }, "filters": [], - "format": "table", + "format": "logs", "global_query_id": "", "parser": "backend", "refId": "A", "root_selector": "", "source": "url", "type": "uql", - "uql": " parse-json\n | scope \"ari://ietf/dtnma-agent/CONST/hello\"\n | extend \"hello\"=array_to_map(\"ari://ietf/dtnma-agent/CONST/hello\",'sw_vendor','sw_version', 'capabilities')\n | extend \"time\"=todatetime(\"time\")\n | project \"time\", \"sw_vendor\"=\"hello.sw_vendor\", \"sw_version\"=\"hello.sw_version\", \"capabilities\"=\"hello.capabilities\"\n | order by \"time\" desc\n", - "url": "http://anms-core:5555/report/entries/table/1/b%27%5Cxf6%27", + "uql": " parse-json\n ", + "url": "report/dictionary/eid/ipn%3A3.6/8401012100", "url_options": { + "body_content_type": "text/plain", + "body_type": "raw", "data": "", "headers": [], "method": "GET", - "params": [ - { - "key": "agent_id", - "value": "1" - }, - { - "key": "nonce_cbor", - "value": "b%27%5Cxf6%27" - } - ] + "params": [] } } ], @@ -801,12 +817,12 @@ "list": [] }, "time": { - "from": "now-6h", + "from": "now-24h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Monitor Page", - "uid": "mwvijjmvk", - "version": 5 + "uid": "mwvijjmvk2", + "version": 8 } \ No newline at end of file diff --git a/grafana/provisioning/datasources/datasource.yml b/grafana/provisioning/datasources/datasource.yml index 3d0d467e..37e273ab 100644 --- a/grafana/provisioning/datasources/datasource.yml +++ b/grafana/provisioning/datasources/datasource.yml @@ -40,11 +40,11 @@ datasources: editable: true isDefault: true -- name: report_entries_table_endpoint +- name: anms-core type: yesoreyeram-infinity-datasource typeName: Infinity access: proxy - url: http://anms-core:5555/report/entries/table/ + url: http://anms-core:5555/ user: database: basicAuth: false From 2db0a8a2d50af2dee0d3e0cbcaca0ae03b194c7c Mon Sep 17 00:00:00 2001 From: David Linko Date: Mon, 6 Apr 2026 19:03:14 -0400 Subject: [PATCH 38/51] updated to include rpt_item_index --- anms-core/anms/components/schemas/ARIs/rpt_entry.py | 1 + anms-core/anms/models/relational/report.py | 5 +++-- deps/dtnma-tools | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/anms-core/anms/components/schemas/ARIs/rpt_entry.py b/anms-core/anms/components/schemas/ARIs/rpt_entry.py index 0263f3e4..b05bbff6 100644 --- a/anms-core/anms/components/schemas/ARIs/rpt_entry.py +++ b/anms-core/anms/components/schemas/ARIs/rpt_entry.py @@ -39,6 +39,7 @@ class Config: time_offset: Optional[str] = None report_source: Optional[str] = None report_items: Optional[list] = None + report_item_indexes: Optional[list] = None # Shared properties class RptEntryFull(RptEntryBase): diff --git a/anms-core/anms/models/relational/report.py b/anms-core/anms/models/relational/report.py index 173236b5..9204103f 100644 --- a/anms-core/anms/models/relational/report.py +++ b/anms-core/anms/models/relational/report.py @@ -46,7 +46,7 @@ class Report(Model): time_offset = Column(LargeBinary) report_source = Column(LargeBinary) report_items = Column(ARRAY(LargeBinary) )#bytea[] NULL - + report_item_indexes = Column(ARRAY(Integer)) # processing the raw cbor into an ari object @orm.reconstructor def init_on_load(self): @@ -66,6 +66,7 @@ def as_dict(self) -> Dict[str, Any]: 'agent_id': getattr(self, 'agent_id'), 'ari_rptlist_id': getattr(self, 'ari_rptlist_id'), 'report_source': getattr(self, 'report_source'), - 'report_items': getattr(self, 'report_items') + 'report_items': getattr(self, 'report_items'), + 'report_item_indexes': getattr(self, 'report_item_indexes') } return dict_obj diff --git a/deps/dtnma-tools b/deps/dtnma-tools index cd454d04..0c2b884b 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit cd454d0467b2b2c9422b3646006a5445e2da5514 +Subproject commit 0c2b884b34ca63b43f439fc155d4b6dbe97ae060 From 4a7c09a79ebeb4125cce11bfc34c2ad067b13fad Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 7 Apr 2026 12:44:11 -0400 Subject: [PATCH 39/51] updated to uses time_offset as a timestamp --- anms-core/anms/components/schemas/ARIs/rpt_entry.py | 2 +- anms-core/anms/models/relational/report.py | 4 ++-- deps/dtnma-tools | 2 +- grafana/provisioning/dashboards/anms-monitor.json | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/anms-core/anms/components/schemas/ARIs/rpt_entry.py b/anms-core/anms/components/schemas/ARIs/rpt_entry.py index b05bbff6..41c8fe94 100644 --- a/anms-core/anms/components/schemas/ARIs/rpt_entry.py +++ b/anms-core/anms/components/schemas/ARIs/rpt_entry.py @@ -36,7 +36,7 @@ class Config: reference_time: Optional[datetime] = None mgr_time: Optional[datetime] = None nonce_cbor: Optional[str] = None - time_offset: Optional[str] = None + time_offset: Optional[datetime] = None report_source: Optional[str] = None report_items: Optional[list] = None report_item_indexes: Optional[list] = None diff --git a/anms-core/anms/models/relational/report.py b/anms-core/anms/models/relational/report.py index 9204103f..0d9b6b91 100644 --- a/anms-core/anms/models/relational/report.py +++ b/anms-core/anms/models/relational/report.py @@ -43,7 +43,7 @@ class Report(Model): agent_id = Column(Integer) ari_rptset_cbor = Column(LargeBinary) ari_rptlist_id = Column(Integer) - time_offset = Column(LargeBinary) + time_offset = Column(DateTime) report_source = Column(LargeBinary) report_items = Column(ARRAY(LargeBinary) )#bytea[] NULL report_item_indexes = Column(ARRAY(Integer)) @@ -51,7 +51,6 @@ class Report(Model): @orm.reconstructor def init_on_load(self): self.nonce_cbor = TRANSMORGIFIER.transcode("0x"+getattr(self, 'nonce_cbor').hex())['uri'] - self.time_offset = TRANSMORGIFIER.transcode("0x"+getattr(self, 'time_offset').hex())['uri'] self.report_source = TRANSMORGIFIER.transcode("0x"+getattr(self, 'report_source').hex())['uri'] self.report_items = [TRANSMORGIFIER.transcode("0x"+x.hex())['uri'] for x in getattr(self, 'report_items')] @@ -65,6 +64,7 @@ def as_dict(self) -> Dict[str, Any]: 'nonce_cbor': getattr(self, 'nonce_cbor'), 'agent_id': getattr(self, 'agent_id'), 'ari_rptlist_id': getattr(self, 'ari_rptlist_id'), + 'time_offset': getattr(self, 'time_offset'), 'report_source': getattr(self, 'report_source'), 'report_items': getattr(self, 'report_items'), 'report_item_indexes': getattr(self, 'report_item_indexes') diff --git a/deps/dtnma-tools b/deps/dtnma-tools index 0c2b884b..d987356b 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit 0c2b884b34ca63b43f439fc155d4b6dbe97ae060 +Subproject commit d987356b8fe64c26a778d1c6a76ee987403dd011 diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json index 26c5b327..3f310d7a 100644 --- a/grafana/provisioning/dashboards/anms-monitor.json +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -24,7 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 0, + "id": 1, "links": [], "panels": [ { @@ -481,7 +481,7 @@ "group": [], "metricColumn": "none", "rawQuery": true, - "rawSql": "-- all reports \nSELECT \n reference_time,\n mgr_time,\n agent_id, \n encode(time_offset,'hex') as time_offset,\n encode(report_source,'hex') as report_source,\n report_items\nFROM\n vw_ari_rpt_set", + "rawSql": "-- all reports \nSELECT \n reference_time,\n mgr_time,\n agent_id, \n time_offset,\n encode(report_source,'hex') as report_source,\n report_items\nFROM\n vw_ari_rpt_set", "refId": "A", "select": [ [ @@ -823,6 +823,6 @@ "timepicker": {}, "timezone": "", "title": "Monitor Page", - "uid": "mwvijjmvk2", - "version": 8 + "uid": "mwvijjmvk", + "version": 2 } \ No newline at end of file From 1b4f28b64bf0d846c3f7b18e1a722889aa852737 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 7 Apr 2026 13:30:33 -0400 Subject: [PATCH 40/51] updated test execset --- checkout-test/test_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkout-test/test_routes.py b/checkout-test/test_routes.py index f83c2bbc..fd1e44dc 100644 --- a/checkout-test/test_routes.py +++ b/checkout-test/test_routes.py @@ -362,7 +362,7 @@ def test_refdm_roundtrip(self): req_headers={ 'content-type': 'text/plain', }, - req_data='ari:/EXECSET/n=1;(//ietf/dtnma-agent/CTRL/inspect)\r\n', + req_data='ari:/EXECSET/n=1;(ari://ietf/dtnma-agent/CTRL/inspect(ari://ietf/dtnma-agent/EDD/num-msg-rx))\r\n', resp_status=[200], ) From a4140b59275ba31cac473f7e66ecfe9cba364003 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 7 Apr 2026 14:29:05 -0400 Subject: [PATCH 41/51] removed special char --- checkout-test/test_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkout-test/test_routes.py b/checkout-test/test_routes.py index fd1e44dc..4274dc89 100644 --- a/checkout-test/test_routes.py +++ b/checkout-test/test_routes.py @@ -362,7 +362,7 @@ def test_refdm_roundtrip(self): req_headers={ 'content-type': 'text/plain', }, - req_data='ari:/EXECSET/n=1;(ari://ietf/dtnma-agent/CTRL/inspect(ari://ietf/dtnma-agent/EDD/num-msg-rx))\r\n', + req_data='ari:/EXECSET/n=1;(ari://ietf/dtnma-agent/CTRL/inspect(ari://ietf/dtnma-agent/EDD/num-msg-rx))', resp_status=[200], ) From 8580547fedfdaa2163a5385f42b60436bd3ff0ce Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 7 Apr 2026 14:56:11 -0400 Subject: [PATCH 42/51] adding logs --- checkout-test/test_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checkout-test/test_routes.py b/checkout-test/test_routes.py index 4274dc89..2483363f 100644 --- a/checkout-test/test_routes.py +++ b/checkout-test/test_routes.py @@ -365,6 +365,7 @@ def test_refdm_roundtrip(self): req_data='ari:/EXECSET/n=1;(ari://ietf/dtnma-agent/CTRL/inspect(ari://ietf/dtnma-agent/EDD/num-msg-rx))', resp_status=[200], ) + LOGGER.info(resp) # TODO this assumes any report is valid without filtering on nonce timer = Timer(5) @@ -377,6 +378,7 @@ def test_refdm_roundtrip(self): break self.assertEqual('text/uri-list', resp.headers.get('content-type')) text = resp.content.decode('utf8') + LOGGER.info(text) self.assertRegex(text, r'^ari:/RPTSET/n=1;.*') From a4ab73b9a7c043b4ef352f0f2389b4254108aa94 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 7 Apr 2026 15:52:54 -0400 Subject: [PATCH 43/51] latest dtnma-tools --- deps/dtnma-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/dtnma-tools b/deps/dtnma-tools index d987356b..3140521d 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit d987356b8fe64c26a778d1c6a76ee987403dd011 +Subproject commit 3140521db51af41724b87fe68c666dc6cf3cbd1b From 8fdc4d6ce3abab0409bb398cdd470a21b122313d Mon Sep 17 00:00:00 2001 From: David Linko Date: Fri, 10 Apr 2026 10:42:00 -0400 Subject: [PATCH 44/51] updated to latest dntma-tools --- deps/dtnma-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/dtnma-tools b/deps/dtnma-tools index 3140521d..aa0147f8 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit 3140521db51af41724b87fe68c666dc6cf3cbd1b +Subproject commit aa0147f80945e9021fc2c8905cac3ed10d5974c9 From da0e94b44595986a8dd174f100c4950907aff09b Mon Sep 17 00:00:00 2001 From: David Linko Date: Fri, 10 Apr 2026 10:53:53 -0400 Subject: [PATCH 45/51] removed rpt_item_index --- .../anms/components/schemas/ARIs/rpt_entry.py | 1 - anms-core/anms/models/relational/report.py | 5 +- anms-core/integration_test/openapi.json | 511 ++++++++++++++++-- 3 files changed, 476 insertions(+), 41 deletions(-) diff --git a/anms-core/anms/components/schemas/ARIs/rpt_entry.py b/anms-core/anms/components/schemas/ARIs/rpt_entry.py index 41c8fe94..632d2ca9 100644 --- a/anms-core/anms/components/schemas/ARIs/rpt_entry.py +++ b/anms-core/anms/components/schemas/ARIs/rpt_entry.py @@ -39,7 +39,6 @@ class Config: time_offset: Optional[datetime] = None report_source: Optional[str] = None report_items: Optional[list] = None - report_item_indexes: Optional[list] = None # Shared properties class RptEntryFull(RptEntryBase): diff --git a/anms-core/anms/models/relational/report.py b/anms-core/anms/models/relational/report.py index 0d9b6b91..265e7ae3 100644 --- a/anms-core/anms/models/relational/report.py +++ b/anms-core/anms/models/relational/report.py @@ -46,7 +46,7 @@ class Report(Model): time_offset = Column(DateTime) report_source = Column(LargeBinary) report_items = Column(ARRAY(LargeBinary) )#bytea[] NULL - report_item_indexes = Column(ARRAY(Integer)) + # processing the raw cbor into an ari object @orm.reconstructor def init_on_load(self): @@ -66,7 +66,6 @@ def as_dict(self) -> Dict[str, Any]: 'ari_rptlist_id': getattr(self, 'ari_rptlist_id'), 'time_offset': getattr(self, 'time_offset'), 'report_source': getattr(self, 'report_source'), - 'report_items': getattr(self, 'report_items'), - 'report_item_indexes': getattr(self, 'report_item_indexes') + 'report_items': getattr(self, 'report_items') } return dict_obj diff --git a/anms-core/integration_test/openapi.json b/anms-core/integration_test/openapi.json index bc197ddf..9d5934a7 100644 --- a/anms-core/integration_test/openapi.json +++ b/anms-core/integration_test/openapi.json @@ -2421,14 +2421,14 @@ } } }, - "/report/all": { + "/report/page": { "get": { "tags": [ "REPORTS", "REPORTS" ], - "summary": "Paged Report", - "operationId": "paged_report_report_all_get", + "summary": "Paged Reports", + "operationId": "paged_reports_report_page_get", "parameters": [ { "required": false, @@ -2460,7 +2460,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_RptEntry_" + "$ref": "#/components/schemas/Page_RptEntryFull_" } } } @@ -2478,24 +2478,24 @@ } } }, - "/report/name/all": { + "/report/all": { "get": { "tags": [ "REPORTS", "REPORTS" ], - "summary": "All Report Name", - "operationId": "all_report_name_report_name_all_get", + "summary": "All Reports", + "operationId": "all_reports_report_all_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "title": "Response All Report Name Report Name All Get", + "title": "Response All Reports Report All Get", "type": "array", "items": { - "$ref": "#/components/schemas/RptEntryBaseInDBBase" + "$ref": "#/components/schemas/RptEntryFull" } } } @@ -2504,22 +2504,116 @@ } } }, - "/report/entry/name/{agent_id}": { + "/report/all/eid/{agent_eid}": { "get": { "tags": [ "REPORTS", "REPORTS" ], - "summary": "Report Def By Id", - "operationId": "report_def_by_id_report_entry_name__agent_id__get", + "summary": "Reports Agent By Name", + "operationId": "reports_agent_by_name_report_all_eid__agent_eid__get", "parameters": [ { "required": true, "schema": { - "title": "Agent Id", + "title": "Agent Eid", + "type": "string" + }, + "name": "agent_eid", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Reports Agent By Name Report All Eid Agent Eid Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/RptEntry" + } + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/report/all/idx/{agent_idx}": { + "get": { + "tags": [ + "REPORTS", + "REPORTS" + ], + "summary": "Reports Agent By Id", + "operationId": "reports_agent_by_id_report_all_idx__agent_idx__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Agent Idx", "type": "integer" }, - "name": "agent_id", + "name": "agent_idx", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Reports Agent By Id Report All Idx Agent Idx Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/RptEntry" + } + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/report/report_source/eid/{agent_eid}": { + "get": { + "tags": [ + "REPORTS", + "REPORTS" + ], + "summary": "Reports Source Agent By Name", + "operationId": "reports_source_agent_by_name_report_report_source_eid__agent_eid__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Agent Eid", + "type": "string" + }, + "name": "agent_eid", "in": "path" } ], @@ -2529,7 +2623,7 @@ "content": { "application/json": { "schema": { - "title": "Response Report Def By Id Report Entry Name Agent Id Get", + "title": "Response Reports Source Agent By Name Report Report Source Eid Agent Eid Get", "type": "array", "items": {} } @@ -2549,30 +2643,76 @@ } } }, - "/report/entries/table/{agent_id}/{nonce_cbor}": { + "/report/report_source/idx/{agent_idx}/": { "get": { "tags": [ + "REPORTS", "REPORTS" ], - "summary": "Report Ac", - "operationId": "report_ac_report_entries_table__agent_id___nonce_cbor__get", + "summary": "Reports Source Agent By Id", + "operationId": "reports_source_agent_by_id_report_report_source_idx__agent_idx___get", "parameters": [ { "required": true, "schema": { - "title": "Agent Id", + "title": "Agent Idx", "type": "integer" }, - "name": "agent_id", + "name": "agent_idx", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Reports Source Agent By Id Report Report Source Idx Agent Idx Get", + "type": "array", + "items": {} + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/report/dictionary/idx/{agent_idx}/{source_cbor}": { + "get": { + "tags": [ + "REPORTS", + "REPORTS" + ], + "summary": "Reports Dictionary By Id And Report Source", + "operationId": "reports_dictionary_by_id_and_report_source_report_dictionary_idx__agent_idx___source_cbor__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Agent Idx", + "type": "integer" + }, + "name": "agent_idx", "in": "path" }, { "required": true, "schema": { - "title": "Nonce Cbor", + "title": "Source Cbor", "type": "string" }, - "name": "nonce_cbor", + "name": "source_cbor", "in": "path" } ], @@ -2581,7 +2721,197 @@ "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "title": "Response Reports Dictionary By Id And Report Source Report Dictionary Idx Agent Idx Source Cbor Get", + "type": "array", + "items": {} + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/report/dictionary/eid/{agent_eid}/{source_cbor}": { + "get": { + "tags": [ + "REPORTS", + "REPORTS" + ], + "summary": "Reports Dictionary By Name And Report Source", + "operationId": "reports_dictionary_by_name_and_report_source_report_dictionary_eid__agent_eid___source_cbor__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Agent Eid", + "type": "string" + }, + "name": "agent_eid", + "in": "path" + }, + { + "required": true, + "schema": { + "title": "Source Cbor", + "type": "string" + }, + "name": "source_cbor", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Reports Dictionary By Name And Report Source Report Dictionary Eid Agent Eid Source Cbor Get", + "type": "array", + "items": {} + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/report/dictionary/search/idx/": { + "post": { + "tags": [ + "REPORTS", + "REPORTS" + ], + "summary": "Reports Dictionary By Search Idx", + "operationId": "reports_dictionary_by_search_idx_report_dictionary_search_idx__post", + "parameters": [ + { + "required": false, + "schema": { + "title": "Start Time", + "type": "string", + "format": "date-time" + }, + "name": "start_time", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "End Time", + "type": "string", + "format": "date-time" + }, + "name": "end_time", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_reports_dictionary_by_search_idx_report_dictionary_search_idx__post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Reports Dictionary By Search Idx Report Dictionary Search Idx Post", + "type": "array", + "items": {} + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/report/dictionary/search/eid/": { + "post": { + "tags": [ + "REPORTS", + "REPORTS" + ], + "summary": "Reports Dictionary By Search Eid", + "operationId": "reports_dictionary_by_search_eid_report_dictionary_search_eid__post", + "parameters": [ + { + "required": false, + "schema": { + "title": "Start Time", + "type": "string", + "format": "date-time" + }, + "name": "start_time", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "End Time", + "type": "string", + "format": "date-time" + }, + "name": "end_time", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_reports_dictionary_by_search_eid_report_dictionary_search_eid__post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Reports Dictionary By Search Eid Report Dictionary Search Eid Post", + "type": "array", + "items": {} + } } } }, @@ -2957,6 +3287,12 @@ } } }, + "500": { + "description": "Error response from NM" + }, + "504": { + "description": "Manager response timed out" + }, "422": { "description": "Validation Error", "content": { @@ -3155,6 +3491,25 @@ } } }, + "/adms/load_default": { + "post": { + "tags": [ + "ADM" + ], + "summary": "Load Default Adm", + "operationId": "load_default_adm_adms_load_default_post", + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, "/alerts/incoming": { "get": { "tags": [ @@ -3451,6 +3806,50 @@ } } }, + "Body_reports_dictionary_by_search_eid_report_dictionary_search_eid__post": { + "title": "Body_reports_dictionary_by_search_eid_report_dictionary_search_eid__post", + "required": [ + "agent_eids", + "source_cbors" + ], + "type": "object", + "properties": { + "agent_eids": { + "title": "Agent Eids", + "type": "array", + "items": { + "type": "string" + } + }, + "source_cbors": { + "title": "Source Cbors", + "type": "array", + "items": {} + } + } + }, + "Body_reports_dictionary_by_search_idx_report_dictionary_search_idx__post": { + "title": "Body_reports_dictionary_by_search_idx_report_dictionary_search_idx__post", + "required": [ + "agent_idxs", + "source_cbors" + ], + "type": "object", + "properties": { + "agent_idxs": { + "title": "Agent Idxs", + "type": "array", + "items": { + "type": "integer" + } + }, + "source_cbors": { + "title": "Source Cbors", + "type": "array", + "items": {} + } + } + }, "Body_update_adm_adms__post": { "title": "Body_update_adm_adms__post", "required": [ @@ -4111,8 +4510,8 @@ } } }, - "Page_RptEntry_": { - "title": "Page[RptEntry]", + "Page_RptEntryFull_": { + "title": "Page[RptEntryFull]", "required": [ "items", "total", @@ -4125,7 +4524,7 @@ "title": "Items", "type": "array", "items": { - "$ref": "#/components/schemas/RptEntry" + "$ref": "#/components/schemas/RptEntryFull" } }, "total": { @@ -4220,43 +4619,81 @@ "properties": { "ari_rptset_id": { "title": "Ari Rptset Id", - "type": "string" + "type": "integer" }, "reference_time": { "title": "Reference Time", "type": "string", "format": "date-time" }, - "report_list": { - "title": "Report List", + "mgr_time": { + "title": "Mgr Time", + "type": "string", + "format": "date-time" + }, + "nonce_cbor": { + "title": "Nonce Cbor", "type": "string" }, - "agent_id": { - "title": "Agent Id", - "type": "integer" + "time_offset": { + "title": "Time Offset", + "type": "string", + "format": "date-time" + }, + "report_source": { + "title": "Report Source", + "type": "string" + }, + "report_items": { + "title": "Report Items", + "type": "array", + "items": {} } } }, - "RptEntryBaseInDBBase": { - "title": "RptEntryBaseInDBBase", + "RptEntryFull": { + "title": "RptEntryFull", "type": "object", "properties": { "ari_rptset_id": { "title": "Ari Rptset Id", - "type": "string" + "type": "integer" }, "reference_time": { "title": "Reference Time", "type": "string", "format": "date-time" }, - "report_list": { - "title": "Report List", + "mgr_time": { + "title": "Mgr Time", + "type": "string", + "format": "date-time" + }, + "nonce_cbor": { + "title": "Nonce Cbor", + "type": "string" + }, + "time_offset": { + "title": "Time Offset", + "type": "string", + "format": "date-time" + }, + "report_source": { + "title": "Report Source", "type": "string" }, + "report_items": { + "title": "Report Items", + "type": "array", + "items": {} + }, "agent_id": { "title": "Agent Id", "type": "integer" + }, + "ari_rptlist_id": { + "title": "Ari Rptlist Id", + "type": "integer" } } }, From ec049ff8c4ab2d9e9be446e75646a34e2e375e4e Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 21 Apr 2026 10:32:18 -0400 Subject: [PATCH 46/51] changed time_offset to agent_time --- .../anms/components/schemas/ARIs/rpt_entry.py | 2 +- anms-core/anms/models/relational/report.py | 4 +- anms-core/anms/routes/ARIs/reports.py | 266 ++++++++++++------ 3 files changed, 185 insertions(+), 87 deletions(-) diff --git a/anms-core/anms/components/schemas/ARIs/rpt_entry.py b/anms-core/anms/components/schemas/ARIs/rpt_entry.py index 632d2ca9..c1cee2ce 100644 --- a/anms-core/anms/components/schemas/ARIs/rpt_entry.py +++ b/anms-core/anms/components/schemas/ARIs/rpt_entry.py @@ -36,7 +36,7 @@ class Config: reference_time: Optional[datetime] = None mgr_time: Optional[datetime] = None nonce_cbor: Optional[str] = None - time_offset: Optional[datetime] = None + agent_time: Optional[datetime] = None report_source: Optional[str] = None report_items: Optional[list] = None diff --git a/anms-core/anms/models/relational/report.py b/anms-core/anms/models/relational/report.py index 265e7ae3..4c614411 100644 --- a/anms-core/anms/models/relational/report.py +++ b/anms-core/anms/models/relational/report.py @@ -43,7 +43,7 @@ class Report(Model): agent_id = Column(Integer) ari_rptset_cbor = Column(LargeBinary) ari_rptlist_id = Column(Integer) - time_offset = Column(DateTime) + agent_time = Column("time_offset", DateTime) report_source = Column(LargeBinary) report_items = Column(ARRAY(LargeBinary) )#bytea[] NULL @@ -64,7 +64,7 @@ def as_dict(self) -> Dict[str, Any]: 'nonce_cbor': getattr(self, 'nonce_cbor'), 'agent_id': getattr(self, 'agent_id'), 'ari_rptlist_id': getattr(self, 'ari_rptlist_id'), - 'time_offset': getattr(self, 'time_offset'), + 'agent_time': getattr(self, 'agent_time'), 'report_source': getattr(self, 'report_source'), 'report_items': getattr(self, 'report_items') } diff --git a/anms-core/anms/routes/ARIs/reports.py b/anms-core/anms/routes/ARIs/reports.py index f6eab289..7a87addf 100644 --- a/anms-core/anms/routes/ARIs/reports.py +++ b/anms-core/anms/routes/ARIs/reports.py @@ -22,10 +22,10 @@ # subcontract 1658085. # -# for handling report set and exec set +# for handling report set and exec set import ace -import ast +import ast import asyncio from cachetools import LFUCache @@ -61,14 +61,25 @@ router = APIRouter(tags=["REPORTS"]) - + # routes for ARIs -@router.get("/page", status_code=status.HTTP_200_OK, response_model=Page[ARIs.RptEntryFull], tags=["REPORTS"]) +@router.get( + "/page", + status_code=status.HTTP_200_OK, + response_model=Page[ARIs.RptEntryFull], + tags=["REPORTS"], +) async def paged_reports(params: Params = Depends()): async with get_async_session() as session: return await paginate(session, select(Report), params) -@router.get("/all", status_code=status.HTTP_200_OK,response_model=List[ARIs.RptEntryFull], tags=["REPORTS"]) + +@router.get( + "/all", + status_code=status.HTTP_200_OK, + response_model=List[ARIs.RptEntryFull], + tags=["REPORTS"], +) async def all_reports(): stmt = select(Report) res = [] @@ -79,12 +90,13 @@ async def all_reports(): # report_source is cbor -async def _report_from_id_source(agent_idx: int, report_source: str, start_time: str = None, end_time: str = None): +async def _report_from_id_source( + agent_idx: int, report_source: str, start_time: str = None, end_time: str = None +): res = [] - report_dict =[] + report_dict = [] - - if(agent_idx): + if agent_idx: if start_time is None: start_time = datetime.fromisoformat("2010-01-01T00:00:00+00:00") if end_time is None: @@ -92,41 +104,58 @@ async def _report_from_id_source(agent_idx: int, report_source: str, start_time: start_time = start_time.replace(tzinfo=None) end_time = end_time.replace(tzinfo=None) - stmt = select(Report).where(Report.agent_id == agent_idx).where(Report.report_source == bytes.fromhex(report_source)).filter(Report.reference_time >= start_time).filter(Report.reference_time <= end_time) + stmt = ( + select(Report) + .where(Report.agent_id == agent_idx) + .where(Report.report_source == bytes.fromhex(report_source)) + .filter(Report.reference_time >= start_time) + .filter(Report.reference_time <= end_time) + ) async with get_async_session() as session: result: Result = await session.scalars(stmt) res = result.all() - if(res): - # translate report_source if its const use its values as the forms for the final report - report_source_ari = TRANSMORGIFIER.transcode("0x"+report_source) - report_source_columns=[f"col {x}" for x,_ in enumerate(res[0].report_items)] - if(isinstance(report_source_ari["ari"], ace.ari.ReferenceARI)): - if(report_source_ari["ari"].ident.type_id == ace.ari.StructType.CONST): - stmt = select(Const.data_value).where(Const.name == str(report_source_ari["ari"].ident.obj_id) ).where(Const.data_model_name == str(report_source_ari["ari"].ident.model_id)).where(Const.namespace == str(report_source_ari["ari"].ident.org_id)) + if res: + # translate report_source if its const use its values as the forms for the final report + report_source_ari = TRANSMORGIFIER.transcode("0x" + report_source) + report_source_columns = [f"col {x}" for x, _ in enumerate(res[0].report_items)] + if isinstance(report_source_ari["ari"], ace.ari.ReferenceARI): + if report_source_ari["ari"].ident.type_id == ace.ari.StructType.CONST: + stmt = ( + select(Const.data_value) + .where(Const.name == str(report_source_ari["ari"].ident.obj_id)) + .where( + Const.data_model_name + == str(report_source_ari["ari"].ident.model_id) + ) + .where( + Const.namespace == str(report_source_ari["ari"].ident.org_id) + ) + ) async with get_async_session() as session: result: Result = await session.scalars(stmt) result = result.all() if result: - report_source_columns = [] - for val in TRANSMORGIFIER.transcode(result[0])["ari"].value: + report_source_columns = [] + for val in TRANSMORGIFIER.transcode(result[0])["ari"].value: report_source_columns.append(val.ident.obj_id) else: - if(isinstance(report_source_ari["ari"].ident, ace.ari.LiteralARI)): - if(isinstance(report_source_ari["ari"].ident.value,list)): - report_source_columns = [] + if isinstance(report_source_ari["ari"].ident, ace.ari.LiteralARI): + if isinstance(report_source_ari["ari"].ident.value, list): + report_source_columns = [] for val in report_source_ari.value: report_source_columns.append(val.ident.obj_id) - - + # data_value for row in res: - new_item = {"reference_time":row.reference_time, - "mgr_time":row.mgr_time, - "time_offset":row.time_offset, - "rpt_set_nonce":row.nonce_cbor, - "report_source":report_source_ari['uri']} - + new_item = { + "reference_time": row.reference_time, + "mgr_time": row.mgr_time, + "agent_time": row.agent_time, + "rpt_set_nonce": row.nonce_cbor, + "report_source": report_source_ari["uri"], + } + for index, value in enumerate(row.report_items): new_item[report_source_columns[index]] = value report_dict.append(new_item) @@ -136,118 +165,187 @@ async def _report_from_id_source(agent_idx: int, report_source: str, start_time: async def _source_from_id(agent_idx: int): res = [] - if(agent_idx): - stmt = select(Report.report_source).distinct().where(Report.agent_id == agent_idx) + if agent_idx: + stmt = ( + select(Report.report_source).distinct().where(Report.agent_id == agent_idx) + ) async with get_async_session() as session: result: Result = await session.scalars(stmt) for x in result.all(): - res.append({"ari":TRANSMORGIFIER.transcode("0x"+x.hex())['uri'], "cbor":x.hex()}) + res.append( + { + "ari": TRANSMORGIFIER.transcode("0x" + x.hex())["uri"], + "cbor": x.hex(), + } + ) return res async def _reports_from_id(agent_idx: int): res = [] - if(agent_idx): + if agent_idx: stmt = select(Report).where(Report.agent_id == agent_idx) async with get_async_session() as session: result: Result = await session.scalars(stmt) res = result.all() return res -@router.get("/all/eid/{agent_eid}", status_code=status.HTTP_200_OK, response_model=List[ARIs.RptEntry], - tags=["REPORTS"]) + +@router.get( + "/all/eid/{agent_eid}", + status_code=status.HTTP_200_OK, + response_model=List[ARIs.RptEntry], + tags=["REPORTS"], +) async def reports_agent_by_name(agent_eid: str): agent_idx = None - agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) + agent_id_stmt = select(RegisteredAgent).where( + RegisteredAgent.agent_endpoint_uri == unquote(agent_eid) + ) async with get_async_session() as session: # Execution set uses URI as agent_id result_agent: Result = await session.scalars(agent_id_stmt) agent_idx = result_agent.one_or_none() - if(agent_idx): - agent_idx = agent_idx.registered_agents_id - + if agent_idx: + agent_idx = agent_idx.registered_agents_id + return await _reports_from_id(agent_idx) -@router.get("/all/idx/{agent_idx}", status_code=status.HTTP_200_OK, response_model=List[ARIs.RptEntry], - tags=["REPORTS"]) +@router.get( + "/all/idx/{agent_idx}", + status_code=status.HTTP_200_OK, + response_model=List[ARIs.RptEntry], + tags=["REPORTS"], +) async def reports_agent_by_id(agent_idx: int): return await _reports_from_id(agent_idx) -@router.get("/report_source/eid/{agent_eid}", status_code=status.HTTP_200_OK, response_model=list, - tags=["REPORTS"]) +@router.get( + "/report_source/eid/{agent_eid}", + status_code=status.HTTP_200_OK, + response_model=list, + tags=["REPORTS"], +) async def reports_source_agent_by_name(agent_eid: str): agent_idx = None - agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) + agent_id_stmt = select(RegisteredAgent).where( + RegisteredAgent.agent_endpoint_uri == unquote(agent_eid) + ) async with get_async_session() as session: # Execution set uses URI as agent_id result_agent: Result = await session.scalars(agent_id_stmt) agent_idx = result_agent.one_or_none() - if(agent_idx): - agent_idx = agent_idx.registered_agents_id - + if agent_idx: + agent_idx = agent_idx.registered_agents_id + reports = await _source_from_id(agent_idx) - return reports + return reports -@router.get("/report_source/idx/{agent_idx}/", status_code=status.HTTP_200_OK, response_model=list, - tags=["REPORTS"]) +@router.get( + "/report_source/idx/{agent_idx}/", + status_code=status.HTTP_200_OK, + response_model=list, + tags=["REPORTS"], +) async def reports_source_agent_by_id(agent_idx: int): reports = await _source_from_id(agent_idx) - return reports + return reports -@router.get("/dictionary/idx/{agent_idx}/{source_cbor}", status_code=status.HTTP_200_OK, response_model=list, - tags=["REPORTS"]) +@router.get( + "/dictionary/idx/{agent_idx}/{source_cbor}", + status_code=status.HTTP_200_OK, + response_model=list, + tags=["REPORTS"], +) async def reports_dictionary_by_id_and_report_source(agent_idx: int, source_cbor: str): reports = await _report_from_id_source(agent_idx, source_cbor) - return reports - -@router.get("/dictionary/eid/{agent_eid}/{source_cbor}", status_code=status.HTTP_200_OK, response_model=list, - tags=["REPORTS"]) -async def reports_dictionary_by_name_and_report_source(agent_eid: str, source_cbor: str): + return reports + + +@router.get( + "/dictionary/eid/{agent_eid}/{source_cbor}", + status_code=status.HTTP_200_OK, + response_model=list, + tags=["REPORTS"], +) +async def reports_dictionary_by_name_and_report_source( + agent_eid: str, source_cbor: str +): agent_idx = None - agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) + agent_id_stmt = select(RegisteredAgent).where( + RegisteredAgent.agent_endpoint_uri == unquote(agent_eid) + ) async with get_async_session() as session: # Execution set uses URI as agent_id result_agent: Result = await session.scalars(agent_id_stmt) agent_idx = result_agent.one_or_none() - if(agent_idx): - agent_idx = agent_idx.registered_agents_id - - reports = await _report_from_id_source(agent_idx, source_cbor) - return reports - + if agent_idx: + agent_idx = agent_idx.registered_agents_id - -# using the known search criteria to filter the reports -@router.post("/dictionary/search/idx/", status_code=status.HTTP_200_OK, response_model=list, - tags=["REPORTS"]) -async def reports_dictionary_by_search_idx(agent_idxs: list[int], source_cbors: list, start_time: datetime=None, end_time: datetime=None): + reports = await _report_from_id_source(agent_idx, source_cbor) + return reports + + +# using the known search criteria to filter the reports +@router.post( + "/dictionary/search/idx/", + status_code=status.HTTP_200_OK, + response_model=list, + tags=["REPORTS"], +) +async def reports_dictionary_by_search_idx( + agent_idxs: list[int], + source_cbors: list, + start_time: datetime = None, + end_time: datetime = None, +): reports = [] for agent_idx in agent_idxs: rpt_cur = [] for source_cbor in source_cbors: - rpt_cur.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) - reports.append({"agent": agent_idx, "reports": rpt_cur} ) - return reports - -@router.post("/dictionary/search/eid/", status_code=status.HTTP_200_OK, response_model=list, - tags=["REPORTS"]) -async def reports_dictionary_by_search_eid(agent_eids: list[str], source_cbors: list, start_time: datetime=None, end_time: datetime=None): + rpt_cur.append( + await _report_from_id_source( + agent_idx, source_cbor, start_time, end_time + ) + ) + reports.append({"agent": agent_idx, "reports": rpt_cur}) + return reports + + +@router.post( + "/dictionary/search/eid/", + status_code=status.HTTP_200_OK, + response_model=list, + tags=["REPORTS"], +) +async def reports_dictionary_by_search_eid( + agent_eids: list[str], + source_cbors: list, + start_time: datetime = None, + end_time: datetime = None, +): reports = [] for agent_eid in agent_eids: rpt_cur = [] agent_idx = None - agent_id_stmt = select(RegisteredAgent).where(RegisteredAgent.agent_endpoint_uri == unquote(agent_eid)) + agent_id_stmt = select(RegisteredAgent).where( + RegisteredAgent.agent_endpoint_uri == unquote(agent_eid) + ) async with get_async_session() as session: # Execution set uses URI as agent_id result_agent: Result = await session.scalars(agent_id_stmt) agent_idx = result_agent.one_or_none() - if(agent_idx): - agent_idx = agent_idx.registered_agents_id + if agent_idx: + agent_idx = agent_idx.registered_agents_id for source_cbor in source_cbors: - rpt_cur.append(await _report_from_id_source(agent_idx, source_cbor, start_time, end_time)) - reports.append({"agent": agent_eid, "reports":rpt_cur} ) - return reports + rpt_cur.append( + await _report_from_id_source( + agent_idx, source_cbor, start_time, end_time + ) + ) + reports.append({"agent": agent_eid, "reports": rpt_cur}) + return reports From 92d775a285c97f25f7fbde926b527e545cdce314 Mon Sep 17 00:00:00 2001 From: David Linko Date: Tue, 21 Apr 2026 14:41:31 -0400 Subject: [PATCH 47/51] updated to align with latest dtnma-tools --- anms-core/anms/models/relational/report.py | 2 +- deps/dtnma-tools | 2 +- .../provisioning/dashboards/anms-monitor.json | 203 +++++++----------- 3 files changed, 77 insertions(+), 130 deletions(-) diff --git a/anms-core/anms/models/relational/report.py b/anms-core/anms/models/relational/report.py index 4c614411..7a3c1c10 100644 --- a/anms-core/anms/models/relational/report.py +++ b/anms-core/anms/models/relational/report.py @@ -43,7 +43,7 @@ class Report(Model): agent_id = Column(Integer) ari_rptset_cbor = Column(LargeBinary) ari_rptlist_id = Column(Integer) - agent_time = Column("time_offset", DateTime) + agent_time = Column( DateTime) report_source = Column(LargeBinary) report_items = Column(ARRAY(LargeBinary) )#bytea[] NULL diff --git a/deps/dtnma-tools b/deps/dtnma-tools index aa0147f8..3a6e7b9a 160000 --- a/deps/dtnma-tools +++ b/deps/dtnma-tools @@ -1 +1 @@ -Subproject commit aa0147f80945e9021fc2c8905cac3ed10d5974c9 +Subproject commit 3a6e7b9ae4aeadcebe2c6e127ba3c005501427a9 diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json index 3f310d7a..bb033a7b 100644 --- a/grafana/provisioning/dashboards/anms-monitor.json +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -251,25 +251,24 @@ }, { "datasource": { - "type": "postgres", - "uid": "amp_core" + "type": "yesoreyeram-infinity-datasource", + "uid": "P1671BF01EA0D6F15" }, "description": "table for displaying all received reports ", "fieldConfig": { "defaults": { "color": { - "mode": "palette-classic" + "mode": "thresholds" }, "custom": { - "align": "left", + "align": "auto", "cellOptions": { "type": "auto" }, "footer": { "reducers": [] }, - "inspect": false, - "minWidth": 100 + "inspect": false }, "mappings": [], "thresholds": { @@ -292,156 +291,91 @@ "id": "byName", "options": "real64_values" }, - "properties": [ - { - "id": "custom.width", - "value": 50 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "real32_values" }, - "properties": [ - { - "id": "custom.width", - "value": 35 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "int_values" }, - "properties": [ - { - "id": "custom.width", - "value": 15 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "vast_values" }, - "properties": [ - { - "id": "custom.width", - "value": 33 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "obj_values" }, - "properties": [ - { - "id": "custom.width", - "value": 17 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "ac_values" }, - "properties": [ - { - "id": "custom.width", - "value": 29 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "tnvc_values" }, - "properties": [ - { - "id": "custom.width", - "value": 0 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "uvast_values" }, - "properties": [ - { - "id": "custom.width", - "value": 126 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "report_id" }, - "properties": [ - { - "id": "custom.width", - "value": 62 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "adm_name" }, - "properties": [ - { - "id": "custom.width", - "value": 100 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "obj_name" }, - "properties": [ - { - "id": "custom.width", - "value": 123 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "agent_id_string" }, - "properties": [ - { - "id": "custom.width", - "value": 114 - } - ] + "properties": [] }, { "matcher": { "id": "byName", "options": "time" }, - "properties": [ - { - "id": "custom.width", - "value": 155 - } - ] + "properties": [] }, { "matcher": { @@ -451,9 +385,16 @@ "properties": [ { "id": "custom.width", - "value": 160 + "value": 189 } ] + }, + { + "matcher": { + "id": "byName", + "options": "items" + }, + "properties": [] } ] }, @@ -472,52 +413,58 @@ "pluginVersion": "12.3.0", "targets": [ { + "columns": [ + { + "selector": "agent_time", + "text": "", + "type": "string" + }, + { + "selector": "agent_id", + "text": "", + "type": "string" + }, + { + "selector": "nonce_cbor", + "text": "", + "type": "string" + }, + { + "selector": "report_source", + "text": "", + "type": "string" + }, + { + "selector": "report_items", + "text": "", + "type": "string" + } + ], "datasource": { - "type": "postgres", - "uid": "amp_core" + "type": "yesoreyeram-infinity-datasource", + "uid": "P1671BF01EA0D6F15" }, - "editorMode": "code", + "filters": [], "format": "table", - "group": [], - "metricColumn": "none", - "rawQuery": true, - "rawSql": "-- all reports \nSELECT \n reference_time,\n mgr_time,\n agent_id, \n time_offset,\n encode(report_source,'hex') as report_source,\n report_items\nFROM\n vw_ari_rpt_set", + "global_query_id": "", + "pagination_mode": "none", + "pagination_param_page_field_name": "page", + "pagination_param_page_field_type": "body_data", + "pagination_param_page_value": 1, + "pagination_param_size_field_name": "size", + "pagination_param_size_field_type": "body_data", + "pagination_param_size_value": 5, + "parser": "backend", "refId": "A", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "column" - } - ] - ], - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - }, - "timeColumn": "time", - "where": [ - { - "name": "$__timeFilter", - "params": [], - "type": "macro" - } - ] + "root_selector": "", + "source": "url", + "type": "json", + "uql": "parse-json", + "url": "report/all", + "url_options": { + "data": "", + "method": "GET" + } } ], "title": "Recieved Reports", @@ -824,5 +771,5 @@ "timezone": "", "title": "Monitor Page", "uid": "mwvijjmvk", - "version": 2 + "version": 1 } \ No newline at end of file From 6ca37f75534a562eddd583690e20b54fc8b04ed4 Mon Sep 17 00:00:00 2001 From: David Linko Date: Wed, 22 Apr 2026 21:13:09 -0400 Subject: [PATCH 48/51] combining reports that are the same ARI but stored with nn or no nn --- anms-core/anms/routes/ARIs/reports.py | 23 ++++++++++--------- anms-core/anms/shared/transmogrifier.py | 4 ++-- .../components/management/agents/reports.vue | 5 ++-- anms-ui/public/app/shared/api.js | 2 +- .../app/store/modules/service_status.js | 2 +- anms-ui/server/components/reports.js | 14 +++++++---- anms-ui/server/core/routes.js | 2 +- .../provisioning/dashboards/anms-monitor.json | 15 ++++++------ 8 files changed, 36 insertions(+), 31 deletions(-) diff --git a/anms-core/anms/routes/ARIs/reports.py b/anms-core/anms/routes/ARIs/reports.py index 7a87addf..343abdbd 100644 --- a/anms-core/anms/routes/ARIs/reports.py +++ b/anms-core/anms/routes/ARIs/reports.py @@ -25,20 +25,13 @@ # for handling report set and exec set import ace -import ast -import asyncio - -from cachetools import LFUCache from fastapi import APIRouter, Depends from fastapi import status from fastapi_pagination import Page, Params from fastapi_pagination.ext.async_sqlalchemy import paginate -from fastapi.responses import JSONResponse from anms.shared.transmogrifier import TRANSMORGIFIER -import io - from sqlalchemy import select, and_ from sqlalchemy.engine import Result @@ -143,7 +136,7 @@ async def _report_from_id_source( if isinstance(report_source_ari["ari"].ident, ace.ari.LiteralARI): if isinstance(report_source_ari["ari"].ident.value, list): report_source_columns = [] - for val in report_source_ari.value: + for val in report_source_ari["ari"].ident.value: report_source_columns.append(val.ident.obj_id) # data_value @@ -165,6 +158,7 @@ async def _report_from_id_source( async def _source_from_id(agent_idx: int): res = [] + hold = {} if agent_idx: stmt = ( select(Report.report_source).distinct().where(Report.agent_id == agent_idx) @@ -172,12 +166,19 @@ async def _source_from_id(agent_idx: int): async with get_async_session() as session: result: Result = await session.scalars(stmt) for x in result.all(): - res.append( + # compiling same ARR that have been stored using with or without NN + curr_uri = TRANSMORGIFIER.transcode("0x" + x.hex()) + hold.setdefault(curr_uri["uri"], []).append(x.hex()) + # hold[curr_uri["uri"]] = hold.get(curr_uri["uri"], []).append() + + for ari,cbor in hold.items(): + res.append( { - "ari": TRANSMORGIFIER.transcode("0x" + x.hex())["uri"], - "cbor": x.hex(), + "ari": ari, + "cbor": cbor, } ) + return res diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index 60210b3e..b6f9c37f 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -205,7 +205,7 @@ def _ace_transcode(self, input): LOGGER.debug(f'decoded as ARI {ari_no_nn}') ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari_no_nn) except Exception as err: - LOGGER.error(f"Error decoding from `{in_text}`: {err}") + LOGGER.warning(f"Error decoding from `{in_text}`: {err} using no NN") ari = ari_no_nn res_obj['cbor'] = in_text @@ -235,7 +235,7 @@ def _ace_transcode(self, input): LOGGER.debug(f'decoded as ARI {ari_no_nn}') ari = ace.nickname.Converter(ace.nickname.Mode.FROM_NN, adms.db_session(), False)(ari_no_nn) except Exception as err: - LOGGER.error(f"Error decoding from `{in_text}`: {err}") + LOGGER.warning(f"Error decoding from `{in_text}`: {err} using no NN") ari = ari_no_nn # rencoding ari to ensure using non nicknames diff --git a/anms-ui/public/app/components/management/agents/reports.vue b/anms-ui/public/app/components/management/agents/reports.vue index 96054184..15c51d23 100644 --- a/anms-ui/public/app/components/management/agents/reports.vue +++ b/anms-ui/public/app/components/management/agents/reports.vue @@ -11,7 +11,7 @@ label="ari" v-model="selected" @option:selected="onReportSelect()"> - + @@ -51,8 +51,7 @@ export default { this.reports= []; await api.methods.apiEntriesForReport(this.registered_agents_id,report_source) .then(res => { - - this.reports = res.data; + this.reports = res.data[0].reports.flat(); }).catch(error => { // handle error console.error("reports error", error); diff --git a/anms-ui/public/app/shared/api.js b/anms-ui/public/app/shared/api.js index e3d2c31d..1e80e50b 100644 --- a/anms-ui/public/app/shared/api.js +++ b/anms-ui/public/app/shared/api.js @@ -155,7 +155,7 @@ export default { }, apiEntriesForReport(obj_agent_id, source_cbor) { - return axios.get(Constants.BASE_API_URL+`report/entries/table/${obj_agent_id}/${source_cbor}`) + return axios.post(Constants.BASE_API_URL+`report/entries/table/${obj_agent_id}`, {'data':source_cbor}) }, apiEntriesForReportTemplate(agentId){ diff --git a/anms-ui/public/app/store/modules/service_status.js b/anms-ui/public/app/store/modules/service_status.js index 9b88b93f..969363ba 100644 --- a/anms-ui/public/app/store/modules/service_status.js +++ b/anms-ui/public/app/store/modules/service_status.js @@ -121,7 +121,7 @@ export default { async setAlert({ state, commit}, index ){ let alert_id = state.alerts[index]["id"] api.methods.apiAcknowledgeAlerts(alert_id); - state.alerts[index]["id"]["visible"]=false; + state.alerts[index]["visible"]=false; }, }, mutations: { diff --git a/anms-ui/server/components/reports.js b/anms-ui/server/components/reports.js index 0fe5da7b..60b24be3 100644 --- a/anms-ui/server/components/reports.js +++ b/anms-ui/server/components/reports.js @@ -20,6 +20,8 @@ * subcontract 1658085. */ +const { response } = require('express'); + (function () { 'use strict'; @@ -44,12 +46,14 @@ exports.getReportEntriesByAgent = async function(req,res,next){ try { + console.log(req.body); let obj_agent_id = req.params.obj_agent_id - let source_cbor = req.params.source_cbor - - const url = utils.generateAnmsCoreUrl(['report','dictionary','idx', obj_agent_id, source_cbor]); - const name_entries = await axios.get(url); - return res.status(200).json(name_entries.data); + let source_cbors = req.body.data + let body = {"agent_idxs": [obj_agent_id],"source_cbors": source_cbors} + // report/dictionary/search/eid/ + const url = utils.generateAnmsCoreUrl(['report','dictionary', 'search', 'idx']); + // obj_agent_id, source_cbor]); + await axios.post(url, body).then(response => {return res.status(200).json(response.data)}); } catch (err) { return next(Boom.badGateway('Error Getting reports', err)); } diff --git a/anms-ui/server/core/routes.js b/anms-ui/server/core/routes.js index fc872a44..c3dcd9c9 100755 --- a/anms-ui/server/core/routes.js +++ b/anms-ui/server/core/routes.js @@ -150,7 +150,7 @@ // --Reports Routes -- // const reports = require('../components/reports') router.get('/report/entry/name/:obj_agent_id', reports.getReportNameByAgent); - router.get('/report/entries/table/:obj_agent_id/:source_cbor', reports.getReportEntriesByAgent); + router.post('/report/entries/table/:obj_agent_id', reports.getReportEntriesByAgent); //------------- Unknown API Routes -------------// router.all('/*', function (req, res, next) { diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json index bb033a7b..e40ea860 100644 --- a/grafana/provisioning/dashboards/anms-monitor.json +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -680,6 +680,7 @@ "type": "yesoreyeram-infinity-datasource", "uid": "P1671BF01EA0D6F15" }, + "description": "Using `report/dictionary/search/eid/` to display `hello` reports", "fieldConfig": { "defaults": { "color": { @@ -740,19 +741,19 @@ "root_selector": "", "source": "url", "type": "uql", - "uql": " parse-json\n ", - "url": "report/dictionary/eid/ipn%3A3.6/8401012100", + "uql": "parse-json\n| mv-expand \"reports\"\n| mv-expand \"reports\"\n| extend \"agent_time\"=\"reports.agent_time\"\n| extend \"sw-vendor\"=\"reports.sw-vendor\"\n| extend \"sw-version\"=\"reports.sw-version\"\n| extend \"capability\"=\"reports.capability\"\n| project \"agent\", \"agent_time\", \"sw-vendor\", \"sw-version\", \"capability\"\n\n", + "url": "report/dictionary/search/eid/", "url_options": { - "body_content_type": "text/plain", + "body_content_type": "application/json", "body_type": "raw", - "data": "", + "data": "{\n \"agent_eids\": [\n \"ipn:3.6\", \"ipn:2.6\"\n ],\n \"source_cbors\": [\n \"8401012100\",\n \"8464696574666b64746e6d612d6167656e74216568656c6c6f\"\n ]\n}", "headers": [], - "method": "GET", + "method": "POST", "params": [] } } ], - "title": "Hello_rpt_agent_1", + "title": "Hello_rpt_agent_dictionary_search", "type": "table" } ], @@ -771,5 +772,5 @@ "timezone": "", "title": "Monitor Page", "uid": "mwvijjmvk", - "version": 1 + "version": 2 } \ No newline at end of file From 4b9213bb2f97f366ffcdd2e2fbff734addbb8b3e Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 23 Apr 2026 11:11:24 -0400 Subject: [PATCH 49/51] adding missing _ in transcoder_put_str --- anms-core/anms/routes/transcoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anms-core/anms/routes/transcoder.py b/anms-core/anms/routes/transcoder.py index 449a96a6..cd6ca8fb 100644 --- a/anms-core/anms/routes/transcoder.py +++ b/anms-core/anms/routes/transcoder.py @@ -143,7 +143,7 @@ async def transcoder_put_await_str(input_ari: str): def transcoder_incoming_str(input_ari: str): return _transcoder_put_str(input_ari) -def transcoder_put_str(input_ari: str): +def _transcoder_put_str(input_ari: str): input_ari = input_ari.strip() transcoder_log_id = None send_to_transcode = False From 659a8da67d84fd8662c657b712e7ed0aa42cc961 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 23 Apr 2026 11:12:02 -0400 Subject: [PATCH 50/51] removed extra logs --- anms-ui/public/app/core/App.vue | 1 - anms-ui/public/app/shared/constants.js | 1 - anms-ui/public/app/store/modules/agents.js | 1 - anms-ui/server/components/reports.js | 1 - anms-ui/server/components/transcoder.js | 2 -- 5 files changed, 6 deletions(-) diff --git a/anms-ui/public/app/core/App.vue b/anms-ui/public/app/core/App.vue index bba73970..ee534ef6 100644 --- a/anms-ui/public/app/core/App.vue +++ b/anms-ui/public/app/core/App.vue @@ -109,7 +109,6 @@ console.log("Calling schedule ARI refresh in App"); this.reloadARIs(); }, status_refresh_rate); - console.log(this.alerts); }, beforeDestroy() { console.log("Clearing interval with id:", this.statusWorkerId); diff --git a/anms-ui/public/app/shared/constants.js b/anms-ui/public/app/shared/constants.js index 59d96178..7b05e553 100644 --- a/anms-ui/public/app/shared/constants.js +++ b/anms-ui/public/app/shared/constants.js @@ -38,7 +38,6 @@ const uiversion = anms_env_config.VUE_APP_UI_VERSION; // const status_refresh_rate = anms_env_config.VUE_APP_STATUS_REFRESH_RATE; //ms -the rate of updating services' status const status_refresh_rate = 60000; //ms -the rate of updating services' status -console.log(status_refresh_rate) const service_info = anms_env_config.SERVICE_INFO; export default Constants; diff --git a/anms-ui/public/app/store/modules/agents.js b/anms-ui/public/app/store/modules/agents.js index d9c05262..07c0d99a 100644 --- a/anms-ui/public/app/store/modules/agents.js +++ b/anms-ui/public/app/store/modules/agents.js @@ -96,7 +96,6 @@ export default { api.methods.apiEntriesForReportTemplate(res.data.registered_agents_id) .then(res => { commit('rptt', res.data) - console.log(res.data) }).catch(error => { // handle error console.error("get agent rptt error", error); diff --git a/anms-ui/server/components/reports.js b/anms-ui/server/components/reports.js index 60b24be3..113d721f 100644 --- a/anms-ui/server/components/reports.js +++ b/anms-ui/server/components/reports.js @@ -46,7 +46,6 @@ const { response } = require('express'); exports.getReportEntriesByAgent = async function(req,res,next){ try { - console.log(req.body); let obj_agent_id = req.params.obj_agent_id let source_cbors = req.body.data let body = {"agent_idxs": [obj_agent_id],"source_cbors": source_cbors} diff --git a/anms-ui/server/components/transcoder.js b/anms-ui/server/components/transcoder.js index f4acbffb..3a59b5e2 100644 --- a/anms-ui/server/components/transcoder.js +++ b/anms-ui/server/components/transcoder.js @@ -111,10 +111,8 @@ const params = {'page': req.query.page, 'size': req.query.size }; const url = utils.generateAnmsCoreUrl(['transcoder', 'db', 'search', encodeURIComponent(transcoderQuery)], params); console.log("[getTranscoderPagedBySearch]: url"); - console.log(url); const transcoderLog = await axios.get(url); console.log("[getTranscoderPagedBySearch]: transcoderLog.data"); - console.log(transcoderLog.data); return res.status(200).json(transcoderLog.data); } catch (err) { From 942d1c42e3b82ed91589a7f27c975d975f9df571 Mon Sep 17 00:00:00 2001 From: David Linko Date: Thu, 23 Apr 2026 15:37:54 -0400 Subject: [PATCH 51/51] addressing sonar issues --- anms-core/anms/routes/ARIs/reports.py | 3 +-- anms-core/anms/shared/transmogrifier.py | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/anms-core/anms/routes/ARIs/reports.py b/anms-core/anms/routes/ARIs/reports.py index 343abdbd..64fb50e0 100644 --- a/anms-core/anms/routes/ARIs/reports.py +++ b/anms-core/anms/routes/ARIs/reports.py @@ -166,10 +166,9 @@ async def _source_from_id(agent_idx: int): async with get_async_session() as session: result: Result = await session.scalars(stmt) for x in result.all(): - # compiling same ARR that have been stored using with or without NN + # compiling same URI that have been stored with or without NN curr_uri = TRANSMORGIFIER.transcode("0x" + x.hex()) hold.setdefault(curr_uri["uri"], []).append(x.hex()) - # hold[curr_uri["uri"]] = hold.get(curr_uri["uri"], []).append() for ari,cbor in hold.items(): res.append( diff --git a/anms-core/anms/shared/transmogrifier.py b/anms-core/anms/shared/transmogrifier.py index b6f9c37f..ec644ea9 100644 --- a/anms-core/anms/shared/transmogrifier.py +++ b/anms-core/anms/shared/transmogrifier.py @@ -23,7 +23,6 @@ from camp.generators import (create_sql) from anms.shared.config import ConfigBuilder -import asyncio import anms.shared.mqtt_client from anms.shared.opensearch_logger import OpenSearchLogger from anms.models.relational import get_session @@ -35,7 +34,6 @@ import traceback import ace import io -import io import json import sqlalchemy @@ -168,7 +166,6 @@ def _transcode_internal(self, input): return ari def _ace_transcode_just_cbor(self, input): - adms = ace.AdmSet() dec = ace.ari_cbor.Decoder() in_text = input.strip() @@ -290,7 +287,6 @@ def _ace_transcode(self, input): def _reload_mqtt(self,adm_name=None): config = ConfigBuilder.get_config() host = config.get('MQTT_HOST') - port = config.get('MQTT_PORT') LOGGER.info('Connecting to MQTT broker %s to notify aricodec' % host)

    39! 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 BbxQyM diff --git a/grafana/provisioning/dashboards/anms-monitor.json b/grafana/provisioning/dashboards/anms-monitor.json new file mode 100644 index 00000000..0439faeb --- /dev/null +++ b/grafana/provisioning/dashboards/anms-monitor.json @@ -0,0 +1,619 @@ +{ + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "9.1.3" + }, + { + "type": "datasource", + "id": "postgres", + "name": "PostgreSQL", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.2.4", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "format": "time_series", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "SELECT\n $__timeGroup(reference_time::timestamp,'1m') as time,\n count(report_list) as value,\n agent_endpoint_uri AS metric\nFROM ari_rptset join registered_agents on ari_rptset.agent_id = registered_agents.registered_agents_id\ngroup by 1, agent_endpoint_uri\norder by time asc", + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "column" + } + ] + ], + "timeColumn": "time", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "title": "Reports per Minute", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.3", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "format": "table", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "SELECT\n *\nFROM registered_agents", + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "column" + } + ] + ], + "timeColumn": "time", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "title": "Agents", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "description": "table for displaying all received reports ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "inspect": false, + "minWidth": 100 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "real64_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "real32_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 35 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "int_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 15 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vast_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 33 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "obj_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 17 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ac_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 29 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "tnvc_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "uvast_values" + }, + "properties": [ + { + "id": "custom.width", + "value": 126 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "report_id" + }, + "properties": [ + { + "id": "custom.width", + "value": 62 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "adm_name" + }, + "properties": [ + { + "id": "custom.width", + "value": 100 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "obj_name" + }, + "properties": [ + { + "id": "custom.width", + "value": 123 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "agent_id_string" + }, + "properties": [ + { + "id": "custom.width", + "value": 114 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "time" + }, + "properties": [ + { + "id": "custom.width", + "value": 155 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "agent_id" + }, + "properties": [ + { + "id": "custom.width", + "value": 160 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 13, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.1.3", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "format": "table", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "-- all reports \nSELECT \n reference_time,\n agent_id, \n report_list\nFROM\n ari_rptset", + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "column" + } + ] + ], + "timeColumn": "time", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "title": "Recieved Reports", + "type": "table" + }, + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.1.3", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "amp_core" + }, + "format": "table", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "SELECT\n*\nFROM vw_obj_metadata\n", + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "column" + } + ] + ], + "timeColumn": "time", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "title": "ARIs", + "type": "table" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Monitor Page", + "uid": "mwvijjmvk", + "version": 2, + "weekStart": "" +} diff --git a/grafana/provisioning/dashboards/dashboards.yaml b/grafana/provisioning/dashboards/dashboards.yaml new file mode 100644 index 00000000..a9f62129 --- /dev/null +++ b/grafana/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,13 @@ +apiVersion: 1 +providers: +- name: 'ANMS Dashboards' + orgId: 1 + folder: '' # This places the dashboard at the root level + type: file + disableDeletion: false + editable: true + updateIntervalSeconds: 30 # Grafana will check for updates every 30 seconds + options: + # The path option specifies the directory where your JSON files are located + # In this case, it can be the current directory ('.') + path: /etc/grafana/provisioning/dashboards diff --git a/grafana/provisioning/datasources/datasource.yml b/grafana/provisioning/datasources/datasource.yml index 66572786..5992fb02 100644 --- a/grafana/provisioning/datasources/datasource.yml +++ b/grafana/provisioning/datasources/datasource.yml @@ -35,20 +35,6 @@ datasources: secureJsonData: password: grafana database: amp_core - ssl_mode: disable - version: 1 - editable: true -- name: amp_core_mysql - type: mysql - access: proxy - url: postgres:3306 - jsonData: - keepCookies: [] - sslmode: disable - user: amp - secureJsonData: - password: amp - database: amp_core - ssl_mode: disable version: 1 editable: true + isDefault: true From e540ba451921f942b644c7f7a4c30b99b11cc3c8 Mon Sep 17 00:00:00 2001 From: David Edell Date: Sat, 29 Nov 2025 16:37:19 -0500 Subject: [PATCH 02/51] Added to UI a prototype Help page and a Not Found page handler. --- anms-ui/public/app/components/help/Help.vue | 36 +++++++++++++++++++ .../app/components/notfound/NotFound.vue | 18 ++++++++++ anms-ui/public/app/core/App.vue | 1 + anms-ui/public/app/core/router.js | 14 ++++++-- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 anms-ui/public/app/components/help/Help.vue create mode 100644 anms-ui/public/app/components/notfound/NotFound.vue diff --git a/anms-ui/public/app/components/help/Help.vue b/anms-ui/public/app/components/help/Help.vue new file mode 100644 index 00000000..02486fbf --- /dev/null +++ b/anms-ui/public/app/components/help/Help.vue @@ -0,0 +1,36 @@ + + diff --git a/anms-ui/public/app/components/notfound/NotFound.vue b/anms-ui/public/app/components/notfound/NotFound.vue new file mode 100644 index 00000000..c9610e69 --- /dev/null +++ b/anms-ui/public/app/components/notfound/NotFound.vue @@ -0,0 +1,18 @@ + + diff --git a/anms-ui/public/app/core/App.vue b/anms-ui/public/app/core/App.vue index ea8c055b..bba73970 100644 --- a/anms-ui/public/app/core/App.vue +++ b/anms-ui/public/app/core/App.vue @@ -33,6 +33,7 @@ Adms + Help diff --git a/anms-ui/public/app/core/router.js b/anms-ui/public/app/core/router.js index 625c027d..5098322b 100644 --- a/anms-ui/public/app/core/router.js +++ b/anms-ui/public/app/core/router.js @@ -22,7 +22,10 @@ import Vue from 'vue'; import Router from 'vue-router'; import Home from '@app/components/home/Home.vue'; -import About from '@app/components/about/About.vue'; +import Help from '@app/components/help/Help.vue'; +import About from '@app/components/about/About.vue' +import NotFound from '@app/components/notfound/NotFound.vue'; + import Constants from '@app/shared/constants'; import User from '@app/components/user/User'; import UserProfile from '@app/components/user/UserProfile'; @@ -57,6 +60,11 @@ export default new Router({ name: 'about', component: About }, + { + path: '/help', + name: 'help', + component: Help + }, { path: '/monitor', name: 'Monitor', @@ -130,8 +138,8 @@ export default new Router({ props: true }, { - path: '*', - redirect: '/' + path: '*', + component: NotFound } ] }); From d8c5af481d9a41367b218bb6ee2ac6e065c65409 Mon Sep 17 00:00:00 2001 From: David Edell Date: Mon, 1 Dec 2025 11:24:05 -0500 Subject: [PATCH 03/51] Resolve default database warning in grafana (hopefully). --- anms-ui/public/app/components/help/Help.vue | 2 +- grafana/provisioning/datasources/datasource.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/anms-ui/public/app/components/help/Help.vue b/anms-ui/public/app/components/help/Help.vue index 02486fbf..b7af1279 100644 --- a/anms-ui/public/app/components/help/Help.vue +++ b/anms-ui/public/app/components/help/Help.vue @@ -10,7 +10,7 @@