PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/saltstack/salt/lib/python3.10/site-packages/salt/utils
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/napalm.py
""" Utils for the NAPALM modules and proxy. .. seealso:: - :mod:`NAPALM grains: select network devices based on their characteristics <salt.grains.napalm>` - :mod:`NET module: network basic features <salt.modules.napalm_network>` - :mod:`NTP operational and configuration management module <salt.modules.napalm_ntp>` - :mod:`BGP operational and configuration management module <salt.modules.napalm_bgp>` - :mod:`Routes details <salt.modules.napalm_route>` - :mod:`SNMP configuration module <salt.modules.napalm_snmp>` - :mod:`Users configuration management <salt.modules.napalm_users>` .. versionadded:: 2017.7.0 """ import copy import importlib import logging import traceback from functools import wraps import salt.output import salt.utils.args import salt.utils.platform try: # will try to import NAPALM # https://github.com/napalm-automation/napalm # pylint: disable=unused-import,no-name-in-module import napalm # pylint: enable=unused-import,no-name-in-module HAS_NAPALM = True try: NAPALM_MAJOR = int(napalm.__version__.split(".")[0]) except AttributeError: NAPALM_MAJOR = 0 except ImportError: HAS_NAPALM = False try: # try importing ConnectionClosedException # from napalm-base # this exception has been introduced only in version 0.24.0 # pylint: disable=unused-import,no-name-in-module from napalm.base.exceptions import ConnectionClosedException # pylint: enable=unused-import,no-name-in-module HAS_CONN_CLOSED_EXC_CLASS = True except ImportError: HAS_CONN_CLOSED_EXC_CLASS = False log = logging.getLogger(__file__) def is_proxy(opts): """ Is this a NAPALM proxy? """ return ( salt.utils.platform.is_proxy() and opts.get("proxy", {}).get("proxytype") == "napalm" ) def is_always_alive(opts): """ Is always alive required? """ return opts.get("proxy", {}).get("always_alive", True) def not_always_alive(opts): """ Should this proxy be always alive? """ return (is_proxy(opts) and not is_always_alive(opts)) or is_minion(opts) def is_minion(opts): """ Is this a NAPALM straight minion? """ return not salt.utils.platform.is_proxy() and "napalm" in opts def virtual(opts, virtualname, filename): """ Returns the __virtual__. """ if (HAS_NAPALM and NAPALM_MAJOR >= 2) and (is_proxy(opts) or is_minion(opts)): return virtualname else: return ( False, f'"{virtualname}" ({filename}) cannot be loaded: ' "NAPALM is not installed: ``pip install napalm``", ) def call(napalm_device, method, *args, **kwargs): """ Calls arbitrary methods from the network driver instance. Please check the readthedocs_ page for the updated list of getters. .. _readthedocs: http://napalm.readthedocs.org/en/latest/support/index.html#getters-support-matrix method Specifies the name of the method to be called. *args Arguments. **kwargs More arguments. :return: A dictionary with three keys: * result (True/False): if the operation succeeded * out (object): returns the object as-is from the call * comment (string): provides more details in case the call failed * traceback (string): complete traceback in case of exception. \ Please submit an issue including this traceback \ on the `correct driver repo`_ and make sure to read the FAQ_ .. _`correct driver repo`: https://github.com/napalm-automation/napalm/issues/new .. FAQ_: https://github.com/napalm-automation/napalm#faq Example: .. code-block:: python salt.utils.napalm.call( napalm_object, 'cli', [ 'show version', 'show chassis fan' ] ) """ result = False out = None opts = napalm_device.get("__opts__", {}) retry = kwargs.pop("__retry", True) # retry executing the task? force_reconnect = kwargs.get("force_reconnect", False) if force_reconnect: log.debug("Forced reconnection initiated") log.debug("The current opts (under the proxy key):") log.debug(opts["proxy"]) opts["proxy"].update(**kwargs) log.debug("Updated to:") log.debug(opts["proxy"]) napalm_device = get_device(opts) try: if not napalm_device.get("UP", False): raise Exception("not connected") # if connected will try to execute desired command kwargs_copy = {} kwargs_copy.update(kwargs) for karg, warg in kwargs_copy.items(): # lets clear None arguments # to not be sent to NAPALM methods if warg is None: kwargs.pop(karg) out = getattr(napalm_device.get("DRIVER"), method)(*args, **kwargs) # calls the method with the specified parameters result = True except Exception as error: # pylint: disable=broad-except # either not connected # either unable to execute the command hostname = napalm_device.get("HOSTNAME", "[unspecified hostname]") err_tb = ( traceback.format_exc() ) # let's get the full traceback and display for debugging reasons. if isinstance(error, NotImplementedError): comment = ( "{method} is not implemented for the NAPALM {driver} driver!".format( method=method, driver=napalm_device.get("DRIVER_NAME") ) ) elif ( retry and HAS_CONN_CLOSED_EXC_CLASS and isinstance(error, ConnectionClosedException) ): # Received disconection whilst executing the operation. # Instructed to retry (default behaviour) # thus trying to re-establish the connection # and re-execute the command # if any of the operations (close, open, call) will rise again ConnectionClosedException # it will fail loudly. kwargs["__retry"] = False # do not attempt re-executing comment = "Disconnected from {device}. Trying to reconnect.".format( device=hostname ) log.error(err_tb) log.error(comment) log.debug("Clearing the connection with %s", hostname) call(napalm_device, "close", __retry=False) # safely close the connection # Make sure we don't leave any TCP connection open behind # if we fail to close properly, we might not be able to access the log.debug("Re-opening the connection with %s", hostname) call(napalm_device, "open", __retry=False) log.debug("Connection re-opened with %s", hostname) log.debug("Re-executing %s", method) return call(napalm_device, method, *args, **kwargs) # If still not able to reconnect and execute the task, # the proxy keepalive feature (if enabled) will attempt # to reconnect. # If the device is using a SSH-based connection, the failure # will also notify the paramiko transport and the `is_alive` flag # is going to be set correctly. # More background: the network device may decide to disconnect, # although the SSH session itself is alive and usable, the reason # being the lack of activity on the CLI. # Paramiko's keepalive doesn't help in this case, as the ServerAliveInterval # are targeting the transport layer, whilst the device takes the decision # when there isn't any activity on the CLI, thus at the application layer. # Moreover, the disconnect is silent and paramiko's is_alive flag will # continue to return True, although the connection is already unusable. # For more info, see https://github.com/paramiko/paramiko/issues/813. # But after a command fails, the `is_alive` flag becomes aware of these # changes and will return False from there on. And this is how the # Salt proxy keepalive helps: immediately after the first failure, it # will know the state of the connection and will try reconnecting. else: comment = ( 'Cannot execute "{method}" on {device}{port} as {user}. Reason:' " {error}!".format( device=napalm_device.get("HOSTNAME", "[unspecified hostname]"), port=( ":{port}".format( port=napalm_device.get("OPTIONAL_ARGS", {}).get("port") ) if napalm_device.get("OPTIONAL_ARGS", {}).get("port") else "" ), user=napalm_device.get("USERNAME", ""), method=method, error=error, ) ) log.error(comment) log.error(err_tb) return {"out": {}, "result": False, "comment": comment, "traceback": err_tb} finally: if opts and not_always_alive(opts) and napalm_device.get("CLOSE", True): # either running in a not-always-alive proxy # either running in a regular minion # close the connection when the call is over # unless the CLOSE is explicitly set as False napalm_device["DRIVER"].close() return {"out": out, "result": result, "comment": ""} def get_device_opts(opts, salt_obj=None): """ Returns the options of the napalm device. :pram: opts :return: the network device opts """ network_device = {} # by default, look in the proxy config details device_dict = opts.get("proxy", {}) if is_proxy(opts) else opts.get("napalm", {}) if opts.get("proxy") or opts.get("napalm"): opts["multiprocessing"] = device_dict.get("multiprocessing", False) # Most NAPALM drivers are SSH-based, so multiprocessing should default to False. # But the user can be allows one to have a different value for the multiprocessing, which will # override the opts. if not device_dict: # still not able to setup log.error( "Incorrect minion config. Please specify at least the napalm driver name!" ) # either under the proxy hier, either under the napalm in the config file network_device["HOSTNAME"] = ( device_dict.get("host") or device_dict.get("hostname") or device_dict.get("fqdn") or device_dict.get("ip") ) network_device["USERNAME"] = device_dict.get("username") or device_dict.get("user") network_device["DRIVER_NAME"] = device_dict.get("driver") or device_dict.get("os") network_device["PASSWORD"] = ( device_dict.get("passwd") or device_dict.get("password") or device_dict.get("pass") or "" ) network_device["TIMEOUT"] = device_dict.get("timeout", 60) network_device["OPTIONAL_ARGS"] = device_dict.get("optional_args", {}) network_device["ALWAYS_ALIVE"] = device_dict.get("always_alive", True) network_device["PROVIDER"] = device_dict.get("provider") network_device["UP"] = False # get driver object form NAPALM if "config_lock" not in network_device["OPTIONAL_ARGS"]: network_device["OPTIONAL_ARGS"]["config_lock"] = False if ( network_device["ALWAYS_ALIVE"] and "keepalive" not in network_device["OPTIONAL_ARGS"] ): network_device["OPTIONAL_ARGS"]["keepalive"] = 5 # 5 seconds keepalive return network_device def get_device(opts, salt_obj=None): """ Initialise the connection with the network device through NAPALM. :param: opts :return: the network device object """ log.debug("Setting up NAPALM connection") network_device = get_device_opts(opts, salt_obj=salt_obj) provider_lib = napalm.base if network_device.get("PROVIDER"): # Configuration example: # provider: napalm_base_example try: provider_lib = importlib.import_module(network_device.get("PROVIDER")) except ImportError as ierr: log.error( "Unable to import %s", network_device.get("PROVIDER"), exc_info=True ) _driver_ = provider_lib.get_network_driver(network_device.get("DRIVER_NAME")) try: network_device["DRIVER"] = _driver_( network_device.get("HOSTNAME", ""), network_device.get("USERNAME", ""), network_device.get("PASSWORD", ""), timeout=network_device["TIMEOUT"], optional_args=network_device["OPTIONAL_ARGS"], ) network_device.get("DRIVER").open() # no exception raised here, means connection established network_device["UP"] = True except napalm.base.exceptions.ConnectionException as error: base_err_msg = "Cannot connect to {hostname}{port} as {username}.".format( hostname=network_device.get("HOSTNAME", "[unspecified hostname]"), port=( ":{port}".format( port=network_device.get("OPTIONAL_ARGS", {}).get("port") ) if network_device.get("OPTIONAL_ARGS", {}).get("port") else "" ), username=network_device.get("USERNAME", ""), ) log.error(base_err_msg) log.error("Please check error: %s", error) raise napalm.base.exceptions.ConnectionException(base_err_msg) return network_device def proxy_napalm_wrap(func): """ This decorator is used to make the execution module functions available outside a proxy minion, or when running inside a proxy minion. If we are running in a proxy, retrieve the connection details from the __proxy__ injected variable. If we are not, then use the connection information from the opts. :param func: :return: """ @wraps(func) def func_wrapper(*args, **kwargs): wrapped_global_namespace = func.__globals__ # get __opts__ and __proxy__ from func_globals proxy = wrapped_global_namespace.get("__proxy__") opts = copy.deepcopy(wrapped_global_namespace.get("__opts__")) # in any case, will inject the `napalm_device` global # the execution modules will make use of this variable from now on # previously they were accessing the device properties through the __proxy__ object always_alive = opts.get("proxy", {}).get("always_alive", True) # force_reconnect is a magic keyword arg that allows one to establish # a separate connection to the network device running under an always # alive Proxy Minion, using new credentials (overriding the ones # configured in the opts / pillar. force_reconnect = kwargs.get("force_reconnect", False) if force_reconnect: log.debug("Usage of reconnect force detected") log.debug("Opts before merging") log.debug(opts["proxy"]) opts["proxy"].update(**kwargs) log.debug("Opts after merging") log.debug(opts["proxy"]) if is_proxy(opts) and always_alive: # if it is running in a NAPALM Proxy and it's using the default # always alive behaviour, will get the cached copy of the network # device object which should preserve the connection. if force_reconnect: wrapped_global_namespace["napalm_device"] = get_device(opts) else: wrapped_global_namespace["napalm_device"] = proxy["napalm.get_device"]() elif is_proxy(opts) and not always_alive: # if still proxy, but the user does not want the SSH session always alive # get a new device instance # which establishes a new connection # which is closed just before the call() function defined above returns if "inherit_napalm_device" not in kwargs or ( "inherit_napalm_device" in kwargs and not kwargs["inherit_napalm_device"] ): # try to open a new connection # but only if the function does not inherit the napalm driver # for configuration management this is very important, # in order to make sure we are editing the same session. try: wrapped_global_namespace["napalm_device"] = get_device(opts) except napalm.base.exceptions.ConnectionException as nce: log.error(nce) return "{base_msg}. See log for details.".format( base_msg=str(nce.msg) ) else: # in case the `inherit_napalm_device` is set # and it also has a non-empty value, # the global var `napalm_device` will be overridden. # this is extremely important for configuration-related features # as all actions must be issued within the same configuration session # otherwise we risk to open multiple sessions wrapped_global_namespace["napalm_device"] = kwargs[ "inherit_napalm_device" ] else: # if not a NAPLAM proxy # thus it is running on a regular minion, directly on the network device # or another flavour of Minion from where we can invoke arbitrary # NAPALM commands # get __salt__ from func_globals log.debug("Not running in a NAPALM Proxy Minion") _salt_obj = wrapped_global_namespace.get("__salt__") napalm_opts = _salt_obj["config.get"]("napalm", {}) napalm_inventory = _salt_obj["config.get"]("napalm_inventory", {}) log.debug("NAPALM opts found in the Minion config") log.debug(napalm_opts) clean_kwargs = salt.utils.args.clean_kwargs(**kwargs) napalm_opts.update(clean_kwargs) # no need for deeper merge log.debug("Merging the found opts with the CLI args") log.debug(napalm_opts) host = ( napalm_opts.get("host") or napalm_opts.get("hostname") or napalm_opts.get("fqdn") or napalm_opts.get("ip") ) if ( host and napalm_inventory and isinstance(napalm_inventory, dict) and host in napalm_inventory ): inventory_opts = napalm_inventory[host] log.debug("Found %s in the NAPALM inventory:", host) log.debug(inventory_opts) napalm_opts.update(inventory_opts) log.debug( "Merging the config for %s with the details found in the napalm" " inventory:", host, ) log.debug(napalm_opts) opts = copy.deepcopy(opts) # make sure we don't override the original # opts, but just inject the CLI args from the kwargs to into the # object manipulated by ``get_device_opts`` to extract the # connection details, then use then to establish the connection. opts["napalm"] = napalm_opts if "inherit_napalm_device" not in kwargs or ( "inherit_napalm_device" in kwargs and not kwargs["inherit_napalm_device"] ): # try to open a new connection # but only if the function does not inherit the napalm driver # for configuration management this is very important, # in order to make sure we are editing the same session. try: wrapped_global_namespace["napalm_device"] = get_device( opts, salt_obj=_salt_obj ) except napalm.base.exceptions.ConnectionException as nce: log.error(nce) return "{base_msg}. See log for details.".format( base_msg=str(nce.msg) ) else: # in case the `inherit_napalm_device` is set # and it also has a non-empty value, # the global var `napalm_device` will be overridden. # this is extremely important for configuration-related features # as all actions must be issued within the same configuration session # otherwise we risk to open multiple sessions wrapped_global_namespace["napalm_device"] = kwargs[ "inherit_napalm_device" ] if not_always_alive(opts): # inject the __opts__ only when not always alive # otherwise, we don't want to overload the always-alive proxies wrapped_global_namespace["napalm_device"]["__opts__"] = opts ret = func(*args, **kwargs) if force_reconnect: log.debug("That was a forced reconnect, gracefully clearing up") device = wrapped_global_namespace["napalm_device"] closing = call(device, "close", __retry=False) return ret return func_wrapper def default_ret(name): """ Return the default dict of the state output. """ ret = {"name": name, "changes": {}, "result": False, "comment": ""} return ret def loaded_ret(ret, loaded, test, debug, compliance_report=False, opts=None): """ Return the final state output. ret The initial state output structure. loaded The loaded dictionary. """ # Always get the comment changes = {} ret["comment"] = loaded["comment"] if "diff" in loaded: changes["diff"] = loaded["diff"] if "commit_id" in loaded: changes["commit_id"] = loaded["commit_id"] if "compliance_report" in loaded: if compliance_report: changes["compliance_report"] = loaded["compliance_report"] if debug and "loaded_config" in loaded: changes["loaded_config"] = loaded["loaded_config"] if changes.get("diff"): ret["comment"] = "{comment_base}\n\nConfiguration diff:\n\n{diff}".format( comment_base=ret["comment"], diff=changes["diff"] ) if changes.get("loaded_config"): ret["comment"] = "{comment_base}\n\nLoaded config:\n\n{loaded_cfg}".format( comment_base=ret["comment"], loaded_cfg=changes["loaded_config"] ) if changes.get("compliance_report"): ret["comment"] = "{comment_base}\n\nCompliance report:\n\n{compliance}".format( comment_base=ret["comment"], compliance=salt.output.string_format( changes["compliance_report"], "nested", opts=opts ), ) if not loaded.get("result", False): # Failure of some sort return ret if not loaded.get("already_configured", True): # We're making changes if test: ret["result"] = None return ret # Not test, changes were applied ret.update( { "result": True, "changes": changes, "comment": "Configuration changed!\n{}".format(loaded["comment"]), } ) return ret # No changes ret.update({"result": True, "changes": {}}) return ret