From 0cf61ca23e5bc29c350242a1b4cb9eaebf5aca53 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 26 Mar 2025 18:31:34 -0500 Subject: [PATCH 01/52] Drop Zerigo DNS support The Zerigo DNS service was permanently shut down on April 30, 2017. --- docs/_static/images/provider_logos/zerigo.png | Bin 23985 -> 0 bytes docs/dns/_supported_methods.rst | 2 - docs/dns/_supported_providers.rst | 2 - libcloud/dns/drivers/zerigo.py | 503 ------------------ libcloud/dns/providers.py | 1 - libcloud/dns/types.py | 1 - .../dns/fixtures/zerigo/create_record.xml | 13 - .../test/dns/fixtures/zerigo/create_zone.xml | 18 - .../zerigo/create_zone_validation_error.xml | 4 - .../test/dns/fixtures/zerigo/get_record.xml | 13 - .../test/dns/fixtures/zerigo/get_zone.xml | 32 -- .../test/dns/fixtures/zerigo/list_records.xml | 54 -- .../zerigo/list_records_no_results.xml | 1 - .../test/dns/fixtures/zerigo/list_zones.xml | 17 - .../fixtures/zerigo/list_zones_no_results.xml | 1 - libcloud/test/dns/test_zerigo.py | 344 ------------ 16 files changed, 1006 deletions(-) delete mode 100644 docs/_static/images/provider_logos/zerigo.png delete mode 100644 libcloud/dns/drivers/zerigo.py delete mode 100644 libcloud/test/dns/fixtures/zerigo/create_record.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/create_zone.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/create_zone_validation_error.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/get_record.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/get_zone.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/list_records.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/list_records_no_results.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/list_zones.xml delete mode 100644 libcloud/test/dns/fixtures/zerigo/list_zones_no_results.xml delete mode 100644 libcloud/test/dns/test_zerigo.py diff --git a/docs/_static/images/provider_logos/zerigo.png b/docs/_static/images/provider_logos/zerigo.png deleted file mode 100644 index ddfbd9d7e16e9d219d995a986d0a00fcbc7db898..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23985 zcmV*GKxw~;P)JU&1@KR`Y}K|eu4KtV%6Lq$SFMngqML`FzOM@dFVNk>Ua zNJ>jdN=!;jPD@QsOioZuPf<=#QcqD+P*PM;Q&dt^RZ~?~R9072R#;V6SyowFSXx|J zTU}aQURzyXTwY*ZUtwNgVqalnU}9upV`XAwWn*P#WM*e&XJ}?DDfi;aqmj*E?tjE<0v zkCBd$l8=#-kdl;mCT$;Qaa$H~jc%FM~j&dSZt%g)fu&(Y4%($CS;(9+b=)78?{*3;G3)YjP5*xJ_F z+t=FM+1uUP+}_*W-`w8d-QVHf;Nsum%z)=;i0>=IH9@>Fem~?CI<7 z>h16A?(psJ@$T^Q@A32S^7QfZ_44%h^Y-}k_xbkt`uF+!`1<_$`~CX-|NH*`{L;?R zX#fBK0b)x>L;#2d9Y_EG010qNS#tmYE+YT{E+YYWr9XB6000McNliru-3t;8CLw6s z3w;0pT3$&+K~xwS?BQpZo#nNt>)yTh=N)5Il}cGCpa9C`9E^>@7-!Ep*V=8h{pqxJ ze%bj2XRm!`+jC8O;*5>S8AK3BDCevSV~qFdy`NF45AK1VzC{7*38(`7 z8L!@sS-*}g?i%#1VR{Ud8~%9;-?CwcKpCpm)BEGAM;oCON1pA(S=enpoKa(dHTS?soBT3>#!k(P;U4q zDSX?s;neHDdEGAG;oH{jA9(4Rch71U0@Wr^+_c2` z55{`Bl@wCA;h&}OZPPQ)|NCnfyECTR(c!=x1xLfryw>Jm2Pxd}&r|r;v<*SIc9T&pYnNe%8~!N@-;!DyPmSpVFaPSDe&7Tsgj?W%IgMZ3 zH9K+NonyFlxZ$6p@a?IM4iCTZ+rP{<&;+fi62?4rXxhW4n3~*LT8tb1ISSv7DAZK; zy!4+R%$h={k1E2Y(tM5{wWH7zeqr*_dNfn_op4l+_9#08p697 zI3wYJ;pWHw(k!1?#|{4!g}+fKc&b7zxY%Idpa1jifuf*FcuS+vE?RZ_^x*?E8f{Ys zm_`%b&c*-KUT;w+C{&ai{*elQBLxH)mO`ZP(O-V`-XI(YXQZ_fZTF6Qe{k}hmp_~Z zBO0Q`@o;~$_|{uGUPUsL8~%X`e+Lakl~BES{x8qG+;%|;HxybTnk`R1z2oAoOXa;m zqXW7+v=S|t?>}qb8((j3I5+%575*Lyg&7%ZKYsl`cDF{0f}udu`q-8ypSXXpW^u39 zy+fel6m24~IQOSnyKmh%2sivg75+YY)Udl(E5~$a~c+UqkWoG7siDiq|Hlf_`k5u?uMF?beXm|hhFS}$$y-?SS+m9X_}(7G>Zo3-|P4T8#M(bP;U5#DEzIG49#qY`(FO- z%hMCnWJEwH1lrtr&!gMQfbMWit2eji;kVBWbB3N?bOP;&*9+(So$eZvBvEeohba7Q zLQ{+(o!O(W{_5EQ=P+$C$6C7Kw(8FB-qXx>Y}iySQ5WA6-Qx!aKr;j83^0v@Gco%%WSjENYzOhJTF0-%Q&sdEzhs`CKg;4FsTI-cH>A%SU#MQ!_MFL_uWv z1HJssfk7}57)_u}yXR#0I}@8V#40!ZLlpi-0!`t-TmS3rK|^Ih5Cy&cU!Pht9+4e& z?=TWUYuo>u{`+2Q!rhu^yAh1c{&ujmv#K#}_y;NcjihSF_x$0nFAkbAbi*;j0q@@V zuTO1O4{5-d6A83qcg6I79SO%2r8*VxYTLhkXQvGd(LiC2a>=+;VC72dOdUp1%yCmR zR8ReNoF|)3AqrI$PgQRCb`-v$ey_oe{wII=^#@g}8*@yxZMzNIpZ?w^Be=GmXHP$W zxDt+`5G@VNydz7t+)B#=;f8NZ;Tx(jP&>c>#oxW!E|Q@JV1UA)yY197!+cR*I5#@$&N#PqBr}Z-Z&R?F{+cIcr9Ai#2Et`Jv zqs>iCHNH%k6ST`8o|$^-ET%YZ0}O{j%>F^gZeMEDLLl7mttos%T4j9Z{onld18ZoR z=UBKcy5*<;a+@g%9%@`&?XLV@S@Qhh;F1!7WAJg?|M>$eQAk0#;oDO9hGv^mKYHd5 zA6c;$j5+2&x$PHEZ*L&dNHMOU1d#MvS@YyrJbR+W@YYmOK-vH2MPs)uZz23YE!9Yn35vKmuZk-=`4M4y7}{wmf{djLpV*Gc%JEd z*Qzrox#8PU_=d*(#H)Y)piT@zm0&~(4R-$QUv7?CD9T7g;p*C^%#;-mcP_ABdqk%@ z9b;uMRMmFZtSwtK3CRuLp2F8wy;$uxhyVP)c8@n++KSP}l9^Uv#p?g#k=5Z+rD2XL zUlNHZ?lulLJrT{+YzAX9oub(oE|Yfr*P{2Q6d<|b+fn$MX2*)u#P+}Z%zIM|yzw9i z`fW2lBX|7p;Y}q(6=z;@6&aY}qSm=9JI^0gyNC}}Nhex!f1~kR)>2gqgmS~*ukbZB zkXB~S{PB-}nQ91~xmFGS+}V1Kc5=Vgk#{{7UTSz$7g>q zxhPN;gmS~*rSLTs20qTAm;dwQ*@l+VVh&h|mfQdP(_4DbEew_G2*MSzyXw1(mi*UY z0(0jQ#n1PD)7s7@>L2`V6!{YXlHD zTYmP`>d+9{rY41J2tZIkl#!Ta`!8mnx!~ZYgh31H`()8#TiOuJNpAT26uu_YIP${p zUYmk2G+e_C4If+m$OEhUvqd_!ZHT5aFPSIIlT1J)C0s1(c=U7p&J)wm&sH}jykSm^ zw@04SnS0g(3Be72r^46N9iDvWFMmDi4ZX(u+R>^MaqX>J(;jK+P{(WSXd7MT314K+ z92064!^yC*!J^xao;lfSO3pzMAjukHv~|&Gp;KDbYQ33SO4UX6 z;D0f=b5Vy%Awap|OG5d23fD0x7C8n-p8xe*GYmmBDgm^o2DbR($+P=9*`{@oON@jB zLC8$pEZS5vlFURAAroyYrs4hB8vVvf8?9HUgQi3resMastTPH#x#4P*uc>eyX|#r) zeD>FW7>ot9ri5cvTbi94J~=!)&F2}P=YTmQ1~_2M({M2GMoXQswnJ2Jq7p%zfAerW zy>t>tAe0;aW`*l0fYy)S{-5v8SXA%gZmK{qao2a3H~Rup2nL1@27@Cn{{F>*DS?IJ5aB*~_fNmGY>#x?61Zvbc}~Ne;1cIE#vBJ@#Gu^v z#Ew`0y1%8EtF>TCc(dnMw)l>Q6jBJ3|NTTlFi)AM%nRlzUqw*9D43@#C|r+D?Rnvk zy9P~!Dk!Q%XtVgfA3eEtXeA=RR9nCRXI?Pke3`SrnPb@a=Jl)6j|>!0KpE-u_x-*z z+`33pNZ}tQn5TTTV4m{Tg0H50ULyQ$To_TVt8g8a(=Y$~>uuA5;%M6_fp+eH`l+ph z3&RvpqGiPR9LI3LoJ)*(Zp?9W=9pVM)46%sxr0+DQ9_kyF&Jw5o@=vpj1>NXlCLNE zdXlS4<|&_wS{8MnkkTv`tHQyubMF|90nEr90LseaEc{ z<~X0@%yH&93!F=wdFEpxbbrzLzkgdxFyVb~XlcT&cjA9e{g3S(!!uJOQ$)EQ7p>k6 z#f@61h0hC=d2ZB#i&0e}nB!O|b3(byG2D!Pk6PL=Wlo@cNdzKF1fYti>Yz&~wJ;}U z7V#OSis43UTBoMlj#Zi&5o>zAqC@XkD}{NXEbuH;v05Y@+spI=MuxYE~hC_n)}a9 zy?YUzZD8E1qKJ|GUO$W1QW;4g5y&I~VI=!LQ6key^s^Cn z6we?Kl5|D{s@3dx?G7diBc?G7F%mo((agvMBQp`23=kxk*M+1GQ_==wYHlou05OJP zXl*YZJa)Wa2U*&%CF|C&8eh^OXlNTQQ5Mt!NY=)fIdf$1kqb)O%GlV=H{ZN;Vhk70 zG>s@MC|pA-RXelC{`l(`2Ig)Am$Y4P$L{~-_ir{TDgkJy5`2ksT?TYZRwsKwwm6&K%tH{_ewlse`te zoLIT(mem`#EX@ufN~v5{xT>HN!|2tIUj45(23C{+a|>YByN~_rqw6DDEsarw&vCx7 zMudRa_{41c;rSM$#Z*Hh)Teg;&*|=l4rl_x*AQt?kN?N}=f)Un5t4vxQDG!Sg@~ZD z==NsW1dVd!x37B<$8fC0gmerNi0Fti=&k$tO-q9$WBd+V=92cF%hSF9*3a8pL28Js)u&Rctqp1U}MMo2p6&+X~(-AC^p zN3BUDl+TMnqs*Q^wCmly$Iec7y{(}$okM$;tl0j@-K(`&xKV-yg{ujOA>(}b%%5JL zF|8;X7>;#g;-UZXotvUiY)~r|P(TU5aEv+5Jj3})F>KMO(Yf`hiRWM5-xO(EMj~9B z7`!L(oz06i6+yW^7q^77dv_lnlYth8Bw+}OB#0^zPIuYjnIuAEYWM4D>6kdyiV7Lo zg(P9NqfPb~&m3=|INHn{+I^Z%845B&gePiRGo{lT>l|OP{k}VH(#itEWO(R}PfGR4 zJ6q+lFgyzuCqH@br4P=`^eN^wfYLv93x+26i1W1Z?00yKcS^6sBMzLo;ic9;#Ip+=y5 zMj3GooUdSj4=Tm!P_`~v-ak1LN<#n+iVohoaPCL9FQp0!gmN94WR#gfjjBkt%nRlO zpj_fL&8S+73B3iT}-2>-NUwEWUmAIjBpHfE4_Pu`n`2l z=@&OaR2c~Zf;kQb0XVp^z??5KXMr#x;LW=GPt3e_v84+HhDKE5V%Lk|2^Cq34K1>UcwFc+;O9Mmmxyk|L0i8Bj9A zhf36QWs%;w5>kMy8AO|%nLspC_>38|=iYk$#e)|zTB3TO0s@$xKR=u8eOp!TQoby2X4b zP)r9oOo$+y&p4lQE-}7BQLm}Pj@7dI{^4Nv3Ff$C(E?3;)Sc+wvM7a-Uwi0uS{%+aE7pOKqE8SEH?%AY>vf3ID$S*SIQ1wn|`WBdR5{H_@9%nq7>AO_T8;KPBP-IXj|$-Gb&RPP^n z^ObilW>Fu6R@GsPDH^SH{p@qOyt`DX4GRjN2|$6&G>wCQ{;yYNwT2M{<}|dd*!X`w zdIzoc22EkmU8+l#O|~d)t2qZG0|MGc360iJ2sf_a7)rSLEt`9iCZKe8uI>Hqxf!Ch zv7x(T+Tybx|2m%9X(%Xe5NTY~po^F3j-DTus=XSZ1eZC`jR3TIFZw-7QX(|Gp>4D( z1S1|?GBZ9^mqrX{+KHs2kl`qJpg;q`oIr&LwbGg1*A6D{EUi(C0#TTm1q+H)!I+~c zoP6ii-<+yUp-ZPr4Ct0Ra6*Y_v^hAvY{TlMt!bMbbt4pN(ZRvLzHo4wwr!1QC*8FP zZQ9U@!p!;MkmmNWD$0Vw6^2Th`s_!~{eIU43rC{iz+ox-_-;ZShB?Z>w*GO1*u^!QE;cb$(WhClPV9LPUjO?Tyuza04+B z0#G!-EeRBfsBXac(Chi!yZ@G@MSxI9G9n-li23#fyG5x94^{P4#+dJHp21*x+7p`G4FMy-(}faeHM+BQkF)zfdh zcER})2M4?L$3Neq=&*sPr7YUfZJO84fCFt4xMFecoePT}zN_O}Br`||fq2DFTSNvYinh3cl{mfOIlty_`%2L ziN1T&-|HK1+Ei+x7DAy?7#?}~rL*Fu+4u3S>o#v%mW}LvA2l z)Kd786xw~S{O;Y>G-w$yMw*rlzxdg?)ho<%MO~3fT9nHW>FuzeJ%g$9Mdqfk?PveGzQKe-pc!f2+3-Vn`2r|zs;#->e)eMI)3=SecL)rEYblI$ z*Z=Zj%dizm)$mX?R3*ur`rY8fMKl;&zg$8QGB7u8`Ndsb5d~yKBT&gq6*9(FjE{j} zF_H;v`0kI_M|N$%ynZLo?0xO|_Z(|!F1&r;U8^)8A?IK~5-H59rN8gx*U#B(w^3Vu z&+R{7v#uLx$*wcgM%XMy{spZ{WKFGbW-m70QPQkf^o zB|=qZR0eeEceHeu zFJE-a@AphMgiq}~w6R1gq3K4>?tkN*3mx-l$L{{ocQ>yrUPW5BB}*3ddiwrQYZ;4U z@7=y$6Oxg_6~>tT@VRGSpM_z7x?{}g-tzQM?-+EfJj97R45~*t&N=B z^X#i1v_^0x2iE-XsjccYn*{YRG}79Bv+f7|&f6DTZ*aVEP?YxL-;E8oj1eI~at#Wu z)H3VPZS|j@IxjE}iO@Kt@0+ z;1LRmfOy+1x=q(kJa--fJ$7bxkyexu5n+yCk)S~2^nqOmTBBQ8{m4(gw*jwVFb)xA z_2XNb|LeF`n%MLH=G9WHHH}u(&%OE4ITW-!{x1(~Sj?acs0b{&-FlPnouf3f$KKwu zY;)7$lEM{+?ctYy{c_vrzzqw)!Qi$hesTL)LZB*!tC3ZUh5~{@fP#~R2n9*M`0<~f zIXW;}x&yAzMA`X+dlrW@xB(hKMllAHo1ZLo_X$w64ItdRO}rH@Tf0GqdU6fQFq&wy z1EGxrp?oUy*ashykl)+)KEq=O6~TPrZ5r;m}88Hf*MGJw23Gg0nxV50FeTs z!%{q&tt)4byyvv^PMx_pCSXp22qPkbdKx2IFMP7=7#ir(2Y&weY6f&+TE+(rPFXen z)CX~4&@|JB_8eW@gH+T_>-4T;l_AQur+>aOfI%@&Vi?^eca1eOZ+a!#gL^iwH|7;S z6)1Huw)a>6{$AS<=9mUeOG7($_kVl%uI^Av?F35Di9k9K1am@B=F~3Us?mfjI``i1 zU-&3c@Bqw#rlo0VT5G$H|G&O7IV`FWiAY0W293fkH!c0m?=C86gM#8!)jyc-|Bv-r zAW*I$Camj zK@Qtg==8b`BUPA_u7PHBZhGwG*+ad)PaWTN_uASPKqEatRRpNrVTWPSy8o3UK+BTb zfBNVuJ--Bk9&SM)7`ylX{r~xER|k6jm3wd3nehT>E$xQ~4g;;-{*xc9M49Ot7|;-m zZDH!{#{=ud#SgZvzk}%sh8+r@(ux-UZ*BQtR%g)h2k?cx}MPEF0C2Erv8 zKmv*a%GA;QC!?$3w>L2%HaA?Hv(|x_}HC4{n3^}sHmbLpj=A< z6h~sP@A==qHE19J2b?(;4$j1bzr1H-scyoE63lZmh*CDU7iZobm^-+I5(LcdV)DV; z3`Mz)ym5F~l(yM^=Nf9;(NM^A)Qcz($~Q5eY#t z!61csDa>1U`|bm^^})g%@S7g`Kki?rRTP9TP(TQ6 z$94vt_h-y$2siLh21hSmXn(N1co^SIL?EX>d~ddP2;2Je?aPU#kU%DL1V}DZzDT(& z6BH5S^DMZ;S8Q8AjujvZBg$8(f?HvD^n=$v>Nm8^G29$5Ak5pb@BG`Nt4l=^5#@^l z1qz*Q*|E3631vjx75x0Wy(N?DIt|~DOVR1QyN-p|@a6YBu)a$cNC6S%2qVgMB?_A9 zRIxM`Cw47%8=+jlH#B5)cmi&m9kr?+~|A>l3-L?#umcr*~Ffz8~kt6SEXnUbA0tz$7Pda5#*5A8rj3g<1rYKb+mTy>l zCe%1{J^AKKr<>NaXhjj_HcBen3=3e-j~9DMhc-E>>b z4F_j|YOCeeryg6K)D+aXnoywB@jHg?;dU0qjgi1<+LJFWS-r#>z8T5k^e36fX+=jc1(CQ$(Pj9UAJX% zOVjHnW;nGydGwqEb!^Q_tJM@3c399*v&V4Ljt`EWE0x*v{ZK{5|Le2+p*F4KfJ1Q_t?mta&(EJ&t`VgG zpM%X-%BY}|3johu8iP%a$UH%&8yu~mY0P@uyCg;Ih}-22fRXZkJm z{E=H$5Eg|6I(_KF0~b{*5MUY@W$O>Wv#ueb1Ya#dIuHqQ?EUB7`1m|%@Tmb7Elp=Q zdE1Yl+&0ej)S>~4^!wi5A4-55BLVIH_in$l$0fq`2n1P6_te|F&b71+%O2d?7@ra0 z5&?wk5EbDnB855GpBW~mO(97XP)!eYG7&T(>baABAaruEDRYER1xSjg=hC~!$G^LA z&C(Sh3Nbr1Ah4C|CxvS%&3ISCICtXIg(XCx0D(Z9{^hFnA|X8Y{^`BGIq1gkY>L9C5&=bpuftT8t8i83=*;PWGJA5w+&>Xj5xC= z4))QO@pUVULV^bK903d(lbe6^okh2-9CxD@Li6J3Y4GuFx2@3NTE>=6YG|})PY)=r zQ9{X7eCG6_(=@c8U^q>%^rf(4V(QFmubu)^R@}dB91Z3PnQCaD z0wG+Ns)lkUF>p~j7Y?0G4QMoxP#_}3fK298tUY_OTA-_UtSnH0U_nX>3KVqZg!N{} z$(jP8TsT`5#02XR3W^3_ zEfjH;#?HU_+ZT^kQ^En~GYU&?{pFK)Fe^Q-&rw`-$(apoCcB3y++#h^X$$(hi=CdWV$LaSp15JsF- zW41panAm*R8i*t`g)ed;%Th3)coI-APC4q`HEYKau0@xwnV10^o}X^bjgnjvvnP+u z5I}3F2E?Xc{>vu3n*4R1O5u{;mM5qC zA2UY*r-iAb7a>&mS_EqUdBW9s&D21+f{+M4{r>LbZ6_!jwyi55fT-XBr~qP-w6m=PIa5aXd&_Dml;fqL6RLD26HG4K=z!9e(w21!ywH4!xn5Jl?D#j1fdjY107pcBg%n=!CqKtd4^r4G)0 zd>m+6)s})z-1Up?v;Cei#wErz7!ZLBy0im5^rv4R7`S2DGS6_(aJ*UnqhH)P5fIwp z`U;Un=(H$H@9G|V?x1M^j?+xHeGnwLrXUftOY}>3X4meI8L49r-aOg3G$@zou%liT z+KC)uRk?z?F*3+f@JK7B89^%{S_=!R7o$-3z4!VjVFT^fWfRnbtEUE;1Zt#%Nntn} zrs173v_K?)+QBiZiYYi0Y81SHhFR3k^cgMvuIpG&p#)dgHe~BB3;AptBz?(t@P20WoiJ#puL5MLD%Jl@IP=mH~ zR?eq_Zx z?xR-@xdDss_}&(CH{~)T90&$`p824(WJ<|4q)P%qwo0@4o|{dW6Nxg?IdkmL>_Cf? zkzz%%p4+?kz@7uAqE_`q_uRIe1aVU!6Hqf#2lL^@sUct@e8y8XQdAUBMhry}P{_i} z*{MoMDJp1i4MH*!N}X*(p(v4{&E)cKRi$N)Gkk5;_!`(m>gIm*FTZ|g(5+|$ z=3Lfd1{=Qj^ZPnJ5M_=A*Coser4z$LZ-02aH6W zB&`%aCq=;-e0+Rd+qR@NIjk~40zyQi6A!OkIj+n}9}X(o*wiO)EXmSPGdxi9_UNvU zPEGZ-sb0D_-LYj68A<|)sA+@;NP-M6UK{}06yq{aRU*}Z02Ig|F(R2WoSuakbSK7y zD#FzzB;ZhogHW^p0_d_;V?k3voxlfgkClgRS)^P~09A|Di-%tM^_w#Ug!35#cNVYw z<>L>IMcXM5iJ@GFG7@CpdgjMp`Tf3gsG>N8GQMI_qJ$>bQK24v|D!X*(mG>1?-?Vq zA);`Zh!PmHv!(>7Qk`xvN1^2S#*5XMBSJfHTisCgy$@7QLMCtTPDdXKu z+ixn7p&Ga(YKqsP2O2<~o^Ao{c9qLg6hsvQ6e1CVIT5J>K6Gepa>bGY7F?Zzm4ZQf zFj7Dv)FxLiBRtI2oRPCH^`{@dZGvE)@7I8&|$sAt>rIdtdnNt_zAYVt})tHPg*M_`!}YRUs4s<+?%v4alh@ z&p-S6u?_~VXn;*@zHhnILZVzp1IOQe{a_G2w&tOSS2zfyut31uPz6;Y7^xbNNuWt& zNa>UUjwm>#)PROS70?W{(@eLhn)jQQMcW?#-uh^*iBJ&8Oy;QI;#qKq&@wh&T(~T~ ziIWH0+74@gx)?RARF-aBXMlPQz>4sdA`w8%)PTaAgzmD{i>E6}01A^c({B&Ez4gnu z9&MDFV{g3p_66J+_v+4Px&v=EJo5OX9jDN`r6^xVqyb9*-0AoK`0QD$+RbU^>5>gw zCv6Ccay@nG(EGd3>C$OBo9^438Id4`1wz)=wG5#UjsXGMG7`|ONNtBROCp7lHnj+9 zEvf`T5N;R|bV{Oh@A}~nZgbpF2NcL8z)VT2j3f}+>1Yb`+D1(4kylR)XQpQcHK}wM zrbQjQ?a9f8k#I1w)+B>#XdCy408|Cs3J?hN7Oh!!R7=IJ8!b9ih96RXG}-Yiv<(m_ zpHZkO8tUHR+$;azPcC5Y%<9n77d_93Or=|jR@bL@3 zfA0dJKvj*_8V2L{?BIITF$lGb`*$B{tyEvU^X?5}!Ixwv(3x!k$N3xw9A=_)l1sAh zLWEk?N`+KRvw8v=I9oSA{QZsHw%2-lA)oHk(f7)5?odr3V#0NL83*YHHAQ< z4NZ{}furD!Bm$valY4@ZDS}b}4J7O2j(gub>IEeNS}^squU}mD$QDafT~LBF7DOVD z&~W61Up;#klrM0+gtPRaUwm)1x+KEapv)6f3Nt5Oc;ih4xi70$YI1n_GLZivNLL#DKIrKHREboN?K_Ds-q$)x9(4nDiN+q<6 zEnE4sySA_B(ljBdfDoFQpkP7}3Z4l@x@WLJ8Oh-VL>nnlhT#|ip{jyHq8fmza&1bw zCn%93Kw(4!sb%TT`%fLN-e@?`1OaFF{3<(ZnpQMZfgqGQ;Q|RVXgKuS-@J7e2nTZ( zI1s^_hkyCqRZ&d=;cJRO(~UU(_OtKI(gZ<68>&vQ^u9-KHLgqWs!V_U=22BzTYB5w zH!I2&Lh%3ww)w$TfHAKNf*?gA7H{3q8KwZ4jbs4B*1D~^GBF5RqSSz~4UgXUz|9lw zP*p(~(Eu`0LK(?q#0-a->0xRxuM?FbB0v%m8jLt-=~#v;o&pv$9VNIHHxW@FLYPyS zqd{S8`%mn59}!?~ii3GQ`9huj?zZ9uGDsj42%;Emm}NcjhyVFTpKi3y=a`#D+g@A0uuxi5%@w0v`pAd?EJAH8!a zZhT3)qruqizub;zVV)TXP#dz0PjH#UFjRw;Th}&SD!u;Hxg$s0J{rj4t&gmppn;)^ zD04K31gb_NBS7hP%K-0RoK3zUqYgBnC2&T}AyEP)+{4YCqNH*SiHfMXBE`~;!X=7_ zu6jt%pF7t&hL-^}s;!@Uc_tU#EEQoyk_BdBg;=npEFRZ%6{K0&Ak&K%V?+F+iE;|yo|p`yXHqV{o?<6W46ItzR00v&0|m8xhRII z1eXNYBM6sRJ@D+G-#Kewj;cGCOx*ReN7fk1b$L6+^!}GWxCkwacigk31Lnq;cmSFq ze2$9(kp_`QRV7d|5n@O*t9Py{P>9(JJ2y|haK6Ie&?|M(wxzSRXH-T)`IL@`qCjWS zLU#$?;?fl-i578*i(z|!wL@iuJW&)~Gz>704 z2_=GBNFt+fiAd?_5U|PRJt9FTdaG6rW}m}hW`DDF?b1#khRS6*0tzYuN@(eJOKXsP zrr@r+X!$7($Fgk=uZ|WFQAt>|XriFno;zP5xCWsPh81(`^a=`v1qvl){pw~cPt4GS zwyl}F(sJUt3xix=tS~2W=44kRPrvy;-#xDkFh0k?3AfvR`Q7!72`CUDh(G{JAP8tx zx6aBWC*m&>l zI@?eSpQn*VG~)(?Uauj#2q+@~0ZIl&h!H3O1J6K9)2M@ndxvN?Jj&E7KGxRrAAPWH z>7uS%#~D#TLqwI46g;D&wJa)wrp0O)>PZ2w1u32NKiod(hgyb9=7rs-7%)DbZV}i` zD{w`_)3M;{j2ND8aoX$@B1+*iVl?etQ^VH|1@53gG~s^ky|FVt+_0h!O9x?I)UFPX zzV+vKj<%*oseHx_nzq^g^aD3x%u|$k%ACTy!kj=^prql`XJ7fRcV=)j7*VH*mi3Q6 z{N(B`1lOcoq7}kVeDr!o7T48xZdrm9f-e(EFwcZ^<2sZGK$#~>t)}ZAIySYd4WF{N zmapE{$y6>2Ad`_ygaS%;tV;tH&WJEluw-KA_@HgWG^50@zrXYP6nI3U%Gi>zUJZiI zm{qPNn4Ri};+Q5B3Ja=aMnTsm<`^g~811RokJrcU8%yC+wV3aJ=})g5c1AS7 z`IMWrw&C$7wsZyx!X?4y36MZxj&eyPp%KRPv0cCZ?Mz`rnPcFr`p(bp+XxkAt)l3e`=Ua2n-Ye|oy?Skm}#jCoq>zCZnD*BQqMT8uBU z+kf=r?aTSvLYb3BRP}>z|JR!r4W|SUit2!sk3MRjAPPmQ5DSl}l>37tsXm`Qzsrt|8F&{Fzo!dSi_;QutI6 zj7(O4cWKi$9@l>Yt~{`A_}#xxuQ=L>YzLyvFiv{Hv4Abd(h zfK=v1kq{HNPyz_;h@X7xPk)_hsF+6J7L0Ts`sE|*P_se=$~6TFxYgsk4l3OkEZ%nS zMhzN5xKcs_s%C_uzziZ!A$67n_?wuP}NhqI6G8qvO zC={iPX+R8)ADx~=Ae7>wWJn-DP#iS@)ndYkQ6y+urjMVP>~U2A)bpoZBj}D>B2f5D z3eZHXT@+`ZKh>IQ>on$|fgXNo(l#&c1oQOND}VT4iq=|2%(=q)<#F(&m?|l5>O$r4FF|H{f6fDktviB@T%j(;< zPbdnZTuE?GG*s2N3gr@IK?G)%5auXEn5o&KP4~U|ZtV_qxc{APlRYj|M*v3=RltpC zCMVBoXkIuwOi`Q>3A6yph$@m%Yoej5;3m+M0m1OZ$@RUed|5y;Jb8RrhM`MWk0lC) z1yvJ>pk}k|{waI$RI7!SmN}~Q_dYxG@Etd^U~ul^UB?2P5yzY_(nBxxuxt|q1QL=2 zRTY9jE~!;k-FbVl6fQ~X;n)B2(jlbgV>n@Z5e^8VENOlwiw~Kibj(4W)!K>~JlajMQ@G z&DT#68n*M%9ZUO7)vO(YD@bGrmpGZJTt|A1P!VCCDv_S0bIUz1od}KSAK8CyWhx6a z5Wo#pAuN?Lv1;9$65gIab&feHM29(T1303Xq2UC92%t=E+;D0JW{(`cuz}zz5@K-n z%%FiK8#XNux1{hzO;9PU`Z53c>`X-m7K%gTuCs4n@ZOS=jZzC!Coj-MIPQQsmpS0x zUf}DReW-_0)6~V{%qzco{um7{rX#hb(ZS-||Hp&dI#tOAQ0QDc?PhLGRK&#(+ z-(9z26fo%Ud4YH%S=6EF6ewIvD070gBib1Xri{1&O3*gTHm>`qt={PIH}2cmHM}W%X>rZlS6Ta2iOUbV7mXmS-B@XXBIEzOa zFBK_3;Y$oCy=~I%{I##N)+$9?Ts3As=&zhuDs87gVhUm4T!VpYD4$W>lo2;&o~r^G z>Q!}c@r~d8;bg_S0kl9vYm2x4_~%=pW(3z`E=mZ)w_iP2JDnL8@7%crgsag&5Md-d z842a9nVFhs5n+z-nRWNSco1!9kH52R1*AeSCo@6Sia`aGvb0m8qxHi618cipRQZxx zEC;Q26CI&cp*Pk`HLoW=xos7mN)_YtlyY>>ktl6+HmoS95n8x{a!E7T@Z&Li<9r1M zG&C)t8UY&}S85y&4-KrC00mgL@$^uo z9)0(YTPA8WDtulQW$NI*Go>>d%kJ2`7=xs66`ts@xAq5&zjUZk%v)1)2lq2CHiNr1 zmq^R%!B=xGalnlQ&WPiHW5I|B7OI*jU;FcO$K5K?*FaT?+Dtz9Z;!2|x)#0;H?rZv z`)?hon$v#M1NSa#g=f_!N??PRn=k;BGJ2R|IssHocZ9xotrgL+>Og> zC};QV+BemtH!HVnn1F~v;VL9jCWowg%zWw`n7K3JhH?1$+1bv`okG-bV~+EcoG)@N zadU8k$V>>;!O1tDefBu!Zh+$rgl~HE@$YPuRtphPlXNF@izGK_Fc);0@K3cKd(nSa*Nd>|qAvCb;uD^ZP zhlIq`CvUCa28{*tf(05E&t4q1G=V_0jn>}!t;@8A#^GICS9LlHxUnEo3+F!GeJ0wr zofuoaj9^aTN{(p=Xf10Vi}K7xP+VIuOqHWUZyvmPk`!$MoNF`ZGY%M^G0Z^&$e>bZ z54`&4SB?XQn_>{Cu7Bz$x394(qO`3B%2#vdoO%8AleA5+{rh)M)WX2{l2As3j8Fv= zM${#gAcd<_0R$CU))Q>Pn1Q zdurG3{(P>XrBQdKK(t!E|HOmKp->f*f^!|B5S)7d=uipmZhdeggMxRt27v?>R3MG( z5m7}{MfifT>Z&WYZ9eU-b_S>SpBYRr;)DXCDuAGlfSYXEwDu$(!!sXz^gv@>7rsdS z?1yiBGVHdHk*E$6Wz8M0pPyCb;5!datfVbXvY>$1Q@dY3GMq@3ZrQn{M=+;wrNW4~ zQLF0er)Hl0^TkjCZ@{78r~f#J?!c^1!Hs!MIADMShSLy^0nUgyBgQ3$x+=KSXVFsK;qSn0PcyX_ZuEXR#u2UNlMN|9FG6oYwmyR*sO&0jy&60wtTFu#YLf}6i+|$*JnN|I@={TKe{tx0tytaA+%&@nS3NJ zyfKB-M2neIp*{G{tctjAvm4t*9o7WzmVn{vz(V=w(DuiaZ2dJ6~m+8u_YffrIGx%W1WNsMm zgs1kPf*Ii-yz%RIXIycO)=LQu5SnE-O`I#eemnj4%B4G3HQpK{B2u$|_|-psI3=Ar z)vVjOiOUMtPz&N>6L(A~uU!OfK~-CC%D{J*6OWv2J5{Z)zzrNwa2A-GItD0&0g3_+ z7+|0SW9Wrb@BHph7t|bOo>kXXo1gyCW(ei$%MODJAG~!Ei0;-0?vpla6mbGXG7ZU$ z3~6d~iG~q@n4O-UuHjk}laLZbGNO@*sE8FpAroOl5WYYoYFWDZ?t?>W=!FkH-nI&q zfbehOUF=8DfAiue-`lxtyi==*OM!av_{l%M_|Bk+4jNQLNy5!2D<692 z=tXGjdtI5mZS-!&zW}XT{8QY0ru?ud%_|Q|2~EHBw6AB!os(8WR>5(lbTX8oa0w z%4Y(Nw%F1K-#HOg%i!>yL*qSB0+P&dP!&R%H|%V_Z}+$`wm9&S@v=M;?A3vRMDgk2maynFMoJClws}O z^Vk*(78I_bfEEdFdmDb>`Q{Xuh66s`H0|Kfg>K&&Y4}VyC<-W5H9%FlB$UgPPbr|< z_|b#^@ys-|VUC01jxN9F7f-G!?h3f@wY5`6_n+_BFzD2e`)JfuTvQ;F5=2I3X=3S? zO+AE+RA73~i+d%*v4+-~NHQ~kHpHx~xMf2x!6OktL{<2_R<(p%v-OsDr=S_0-E&}# znx-j&s0NvkT-J7$-MjbPVU^(!&b<85!JV7OyeaA0Kl0&wAD_sfwu7dWL3iW5H>*<> zN{zDOzCHc3y*^|8k53=H{r1f(7ftB!;<3|*-hJhhnNXS;wtoNqWq@)?;Tk%iil+{m zjX$XM?HL@$x;~&abEXHFW7_fs?u=NdyE(YI0S?YpIN%mXUV8WU7p!RqEKrBB?LT~M zjbNe>#@E##e&!qv9?a}JQ(DQ0OjM0XNXR6yQf~d>;^ony^+ajUy>qVPSw$Mns!cGG znd%W@MIVY~1u?=S5)TnZgv-(!ic*$uUOp3K_|b#6E$u|V0TEFRAQ3L>QZ_tr@Z#A( zVQ_l(KVMqCe8tkz4&wa511G0ig)sVqX8ofNtJrL=!dEhD+9;4(DunV4l+rTpv!GLbA=SjI ztC~qT;vk4%lrmi=81VodtLLk`*3`zll|~UEGc6*;OjqupRKP(572%6I6%=f2`GMaIu9y@*KRAD826WKS)0vpu`Owd{ zTa+q=S}c^c_aE)=t&92uGy6|`vUl||KY!-f>4U>ibHtb4^Z1VOin5?^EkOpRG_>8V zu{?Kw(n8xpqtP1XR=TTKH)G73sb0DogC^5akx&W8N>x?JOc4Q+k;tIml-gENn>|0X zH>O}f(~WSfSnEx9;}3tluA^Os9Z(46>xp5nxHiK!3NdZkHfmnoP}K+s6ckEjYDg$# zB$h;bA!xknpwy~bM3l@dnTVTs=^>GBqN0 z&>NpRKYLzR3Mp3T;zWAw!#{c8meQiN z!zIeslm%NfJljyjY0xe>rJzAoE!sy>6`_Q?s2kFq)V32P0zR`@ECKtT*1x8d^#>3ReWtuphavJ5kzn@K@bE10vSPA z;6O%ZHd3*y2c~gzN0}2r6Em#+{$t-?T?jJyhBCd@h)@v&R1gFqHGs&1sE7b!W>}Ts z>@XO1H3BnjL?FdGqADqwASO*WAQZe?5KC(!!YF*nuxz}2-*Fgn;ov8m*W-zr;$3R> zOfIXMQJQU&?Xnk7*N7mZqSj;`f@aOisMNwj? z-I!f`%m29l)^YG^EGS%?29kh4(ea11`Q6DD9944&XLf(1H%~6>&_IuQ0Vycg5el^s z7mn}QeFk#}lmOM1$wz;2{|2ZcVthkVz)`7$0u>ZcARs^i1$r@3K};eWXTV6b8lXY& z8MREOP0|hb>8U{sT5B^xpccMJI$Iui{g^bZ4iA5H$C|MgydpUkp=jYU6{ME&%|BTj zZ=V=wH^=}rk5I)ubotIlAHHi9BL)Zx8iIvx=-K#7C#E2RPzn*joO0Vwf3SXnP)jZ= zd<6}}Es|QaE`2aBK6jd?i3l26!)SNUE~>d|tPZo6kR-{7h%iSWK@bS$rErOBv=K)> z`S+LiV}N6!3L1<*@Z{ZV8Aj>w%_aD-hBppM0HKVi6w(7_-mu2d>mYlSs7^31K+#Z2 zI&+#7$&3h80z}oCbVx*eV%DQ{L_uws)p`Hbt%Mx{6_3(I_Hov&RRU)obfge$epvaS1$pT)9XICXYx45dtr&w7qeN^2+!LMec* z*Tu_U-#68X2vwD#N2rZ=H}81t{*4_55(OelC?gu`Dag82wyNyAFcVRT0JI(JF1`KH z?`>!Rfm&El_(}>IC4$6du%7}ZnX=C>=n=(mffC$1!k`l}jkOW~Qn3odF zsaoR1>%V>RNQ;Kk1Q0khJAUxpTT7?~5g6Z4fwK1QrL^Ih8R2pXp{hcpCOJZb?c6ZY zNMh^mT!jn~$V?Ga1(e7{L{bF0WB05wG_x$-e9zJtG2E%RW8%e0BU#R(oJ{0{&9Qu_=O0ix=k!!y>7*0>o+eqFolRTluMLB zjDd9@S-O1p;UkC75d@`amfW&-?LBv_PB#jlSGXQeRU#|Ks_)HAzc}4CC89fMoIzvE z9#WIrZYm&xG7<^Gh;kJvXpq^<*<)}0?&U)*w2UYL7)y5k@bQ~9sFGDo`KAg?-o5fd zA`+1z!H6JLM1@31GGWk>rMHfu#L@@W%|b(xjHsxPD48H+h^BOVOV)~8cwO`G+Oydd zx%s9@i{wirChuEybgEM#CvRTWm87@viJj9;E?IPQ;d8WxNSmdza@oc^j~qDi(a}EX z<4oLq%Z7DJwoVi(pwNaXpE3nP$M0Bq@5v({?LU$3bSK8{-@b0q>gD*bDU1{@D_oC9 zK|Qm}bXoR&rr$dYEyc#ltU+*b_i$p-Sf^nm5>HJBHOupmjK!NrG-#`wP;)tl-V)dO@k{973E~3k0-``+UhqYVXX+TO7$VALq09+offAV&MF~12BI$Gp zF&E{F6vMO17+}UoBoz^*1Vo3+I)Da5B+?3#liQh7MN~y-paWP#LLI5#@6V*CotRuLy<5E}q#P3N5OD1A`;)E-4R8HW5%j1LZ0L5}Zsc(H?#6 zPw$>;&2Z+pH9GL-p(h^NpkNgK;RFN(MEIOgAov2|Q^s`!BcUu%0%eYZ@+C?FLSddF z2+H*YhyY5%97mF?DO``Fa7m^}DR=nrHy_NJIuVSR;%9fMuh_iAB9%ZD0_BT>goHvO z^v}Hb+vktBPy!UkRKuGGe*C>14W4fN%`^m*uR*wqlu&}?8iIMMA)s8zK_FeYOqo-~ z6g&%Gq#=o*EGUeGBV1iDFF-`WMS(~mg=9pO&nsM)2p}K{qHNdk|9q=zH*hdhFhBQU zStHxKMnnp8k}nZb5)jGhy&wMS)x+9^Mz}!(I$+sDPk(n)6H&}t8vZ`YykMSkB@Gl1 z1m!a+0m%Z6^yOOTfE{qbQOEI(v4QWc^t;!vS;WBg1v%lZn0aC-o8fWI5=~}Gk z^}6u8k=$SVP)EIp3onJc2yg3lz_NH;x7_& z1j3zyYs0=PS5JVFGDb9HcK)blwrzWaj)4P#Zy+23e)ia*_b*e?RAP=}7^{EytFrmm&dQj0ap|VgVlR} z^1W5TisVfZOGeyD+$0t#azo@^;=2&@A|UWJagW426e3c#(XCVVk5^_IsDwjPMQcva zgol^$U~-2daTkX|0eW8@d-vC85;F#dNr(z<`@ya?Kz0d1P7Ife8(a*V7b4%r5qER( zU5Rg%n1|$!`b~-tyfE24aEuBvE%_EM&tfHaBgz}9XZC-8jUZCd8mz>;rQB3$<<8)5|G|YCz>EsB zGe^?&%j-w*AaNIgNzL_-esO3DA|Nopjm}@~{n3+pC6vC`v}NbRLTwG&%d~53vNmnk=8&Z=1D-ZQdqg8 z+x*KXjYU;vM8guzm(wG;yM8p$>)_&cM&kT$4j#NnX;4$S5hZWvJpa?(>pOUe5NQoq zVxHt4Vu)l$o_M9t?=j;~vaoCa-d(|lL@5Cmw}~;IJ9^;Y#fFB88=~mJ*p@wewg7?F zFeK*7ExMP8+sIRC{GAimXd+m|@Cq&+PuKd7kJXfbi`!f~_3_*9|2I`bg-0atXb*1R zyZceBq-Yt5)_^7EO9^lbHB7EtHa2@|3dyk;4S|1tZ8p{$#*7lUjm?}r{?^-PqE*q9 zts9Da6YI{lS6+Xd+;m-$IXTf9sKk5-5Fm<0px(HQw?E7&Ta4lsKXy7q6c`nH4R90YG~LC|PlAyJpKLF2oH6 zqWSX5>}+@agA}leoNIbhM-RPs+J##bxPqD{KJ?71udNd~6kN21BC$XPA`acB3IBAG zZiZ+?i=^Zij`<4jc5!lro8`~{^Wc%wK9aLV<{15Ymh{yAU610PAbU+~C=v@)0};z< z{Mr80<#T=H#vH>1XT4(+>qh`r?e^8-gCCq~I?9H;sfcE7)w6HxdkkDaqBSIm1C$gMi{o~628miiY?9Q@T;D&`266wP7B{#Q2CP8Z|}a-ua9 zi3Jh2L}qI_W6v=A*(sVR>-3QK06TYV#YFw^*z|Az`PUcKSfxx-_K|MyI+GiJwtZ7_ zDJMukqBR_eMH&HvHSzo}KBePSnVVumb>VP-YRif*4jnn;scLY*8Jp=Vrme62a5CW~ zSfVvliAD3$ug5n2I5(eO%m|ARfl4zcFP;DW#2^3dQ;P_MBhL+S`PRREa~j&R@rzeuQslts5ADtMvbj2#%qd)}pqE>8wbN@Q}R#hNc!_60IRiERrWBpnp9oJ(l|CPa?p;<-%!z(s4Q5{s0a zLZD8{Ua#F%|Lf5&JJnSx+~ugI4?atD=y1J$;_r8@#3cSiw1z7&AjL|cvw1~#`GK?E z$xS7YHBE;~?nXCQ&*HWpy}XvzQc5hAJb;KEaE)#3cBkLD*6H*rL(Iv58)RJLo8EYH zGucDJMQceU2F0a3tC3aDUA4nkHQvJ=M{t zmUBa1``o^l9!+G4>{X<-G!lanarfXUYw`!x1Hb#jwJXet#toy(CwA@Ku|ZAtQN77?M~#A?&%*dv=hJ9){75;Tl$-ni@OCsxTr1-PJJ(^?XV0g2#*Al${nFFpUo zZ$JFwtY5nttC8{5Pdv5m=}uR8$q5A1w3bF6hMbz3gXTs?njq#j?ndh(%V zs)P|lYiT70Ots93V$I5&XQr;M3Y}#W5nND^Xf3V8z@_BgFOjZbO-b${V$BFRL9~`w zV!&ong#<2g2?` VULTR single region driver :mod:`libcloud.dns.drivers.vultr` :class:`VultrDNSDriver` `World Wide DNS`_ :doc:`Click ` WORLDWIDEDNS single region driver :mod:`libcloud.dns.drivers.worldwidedns` :class:`WorldWideDNSDriver` -`Zerigo DNS`_ ZERIGO single region driver :mod:`libcloud.dns.drivers.zerigo` :class:`ZerigoDNSDriver` `Zonomi DNS`_ :doc:`Click ` ZONOMI single region driver :mod:`libcloud.dns.drivers.zonomi` :class:`ZonomiDNSDriver` ================= ========================================= ================= ==================== ======================================== ============================== @@ -53,5 +52,4 @@ Provider Documentation Provider Constant Su .. _`Route53 DNS`: http://aws.amazon.com/route53/ .. _`Vultr DNS`: https://www.vultr.com .. _`World Wide DNS`: https://www.worldwidedns.net/ -.. _`Zerigo DNS`: http://www.zerigo.com/ .. _`Zonomi DNS`: https://zonomi.com diff --git a/libcloud/dns/drivers/zerigo.py b/libcloud/dns/drivers/zerigo.py deleted file mode 100644 index 0f37e0f357..0000000000 --- a/libcloud/dns/drivers/zerigo.py +++ /dev/null @@ -1,503 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. - -__all__ = ["ZerigoDNSDriver"] - - -import copy -import base64 - -from libcloud.dns.base import Zone, Record, DNSDriver -from libcloud.dns.types import Provider, RecordType, ZoneDoesNotExistError, RecordDoesNotExistError -from libcloud.utils.py3 import ET, b, httplib -from libcloud.utils.xml import findall, findtext -from libcloud.utils.misc import get_new_obj, merge_valid_keys -from libcloud.common.base import XmlResponse, ConnectionUserAndKey -from libcloud.common.types import LibcloudError, InvalidCredsError, MalformedResponseError - -API_HOST = "ns.zerigo.com" -API_VERSION = "1.1" -API_ROOT = "/api/%s/" % (API_VERSION) - -VALID_ZONE_EXTRA_PARAMS = ["notes", "tag-list", "ns1", "slave-nameservers"] -VALID_RECORD_EXTRA_PARAMS = ["notes", "ttl", "priority"] - -# Number of items per page (maximum limit is 1000) -ITEMS_PER_PAGE = 100 - - -class ZerigoError(LibcloudError): - def __init__(self, code, errors): - self.code = code - self.errors = errors or [] - - def __str__(self): - return "Errors: %s" % (", ".join(self.errors)) - - def __repr__(self): - return "".format( - self.code, - len(self.errors), - ) - - -class ZerigoDNSResponse(XmlResponse): - def success(self): - return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED] - - def parse_error(self): - status = int(self.status) - - if status == 401: - if not self.body: - raise InvalidCredsError(str(self.status) + ": " + self.error) - else: - raise InvalidCredsError(self.body) - elif status == 404: - context = self.connection.context - - if context["resource"] == "zone": - raise ZoneDoesNotExistError(value="", driver=self, zone_id=context["id"]) - elif context["resource"] == "record": - raise RecordDoesNotExistError(value="", driver=self, record_id=context["id"]) - elif status != 503: - try: - body = ET.XML(self.body) - except Exception: - raise MalformedResponseError("Failed to parse XML", body=self.body) - - errors = [] - - for error in findall(element=body, xpath="error"): - errors.append(error.text) - - raise ZerigoError(code=status, errors=errors) - - return self.body - - -class ZerigoDNSConnection(ConnectionUserAndKey): - host = API_HOST - secure = True - responseCls = ZerigoDNSResponse - - def add_default_headers(self, headers): - auth_b64 = base64.b64encode(b("{}:{}".format(self.user_id, self.key))) - headers["Authorization"] = "Basic %s" % (auth_b64.decode("utf-8")) - - return headers - - def request(self, action, params=None, data="", headers=None, method="GET"): - if not headers: - headers = {} - - if not params: - params = {} - - if method in ("POST", "PUT"): - headers = {"Content-Type": "application/xml; charset=UTF-8"} - - return super().request( - action=action, params=params, data=data, method=method, headers=headers - ) - - -class ZerigoDNSDriver(DNSDriver): - type = Provider.ZERIGO - name = "Zerigo DNS" - website = "http://www.zerigo.com/" - connectionCls = ZerigoDNSConnection - - RECORD_TYPE_MAP = { - RecordType.A: "A", - RecordType.AAAA: "AAAA", - RecordType.CNAME: "CNAME", - RecordType.GEO: "GEO", - RecordType.MX: "MX", - RecordType.NAPTR: "NAPTR", - RecordType.NS: "NS", - RecordType.PTR: "PTR", - RecordType.REDIRECT: "REDIRECT", - RecordType.SPF: "SPF", - RecordType.SRV: "SRV", - RecordType.TXT: "TXT", - RecordType.URL: "URL", - } - - def iterate_zones(self): - return self._get_more("zones") - - def iterate_records(self, zone): - return self._get_more("records", zone=zone) - - def get_zone(self, zone_id): - path = API_ROOT + "zones/%s.xml" % (zone_id) - self.connection.set_context({"resource": "zone", "id": zone_id}) - data = self.connection.request(path).object - zone = self._to_zone(elem=data) - - return zone - - def get_record(self, zone_id, record_id): - zone = self.get_zone(zone_id=zone_id) - self.connection.set_context({"resource": "record", "id": record_id}) - path = API_ROOT + "hosts/%s.xml" % (record_id) - data = self.connection.request(path).object - record = self._to_record(elem=data, zone=zone) - - return record - - def create_zone(self, domain, type="master", ttl=None, extra=None): - """ - Create a new zone. - - Provider API docs: - https://www.zerigo.com/docs/apis/dns/1.1/zones/create - - @inherits: :class:`DNSDriver.create_zone` - """ - path = API_ROOT + "zones.xml" - zone_elem = self._to_zone_elem(domain=domain, type=type, ttl=ttl, extra=extra) - data = self.connection.request( - action=path, data=ET.tostring(zone_elem), method="POST" - ).object - zone = self._to_zone(elem=data) - - return zone - - def update_zone(self, zone, domain=None, type=None, ttl=None, extra=None): - """ - Update an existing zone. - - Provider API docs: - https://www.zerigo.com/docs/apis/dns/1.1/zones/update - - @inherits: :class:`DNSDriver.update_zone` - """ - - if domain: - raise LibcloudError("Domain cannot be changed", driver=self) - - path = API_ROOT + "zones/%s.xml" % (zone.id) - zone_elem = self._to_zone_elem(domain=domain, type=type, ttl=ttl, extra=extra) - response = self.connection.request(action=path, data=ET.tostring(zone_elem), method="PUT") - assert response.status == httplib.OK - - merged = merge_valid_keys( - params=copy.deepcopy(zone.extra), - valid_keys=VALID_ZONE_EXTRA_PARAMS, - extra=extra, - ) - updated_zone = get_new_obj( - obj=zone, klass=Zone, attributes={"type": type, "ttl": ttl, "extra": merged} - ) - - return updated_zone - - def create_record(self, name, zone, type, data, extra=None): - """ - Create a new record. - - Provider API docs: - https://www.zerigo.com/docs/apis/dns/1.1/hosts/create - - @inherits: :class:`DNSDriver.create_record` - """ - path = API_ROOT + "zones/%s/hosts.xml" % (zone.id) - record_elem = self._to_record_elem(name=name, type=type, data=data, extra=extra) - response = self.connection.request( - action=path, data=ET.tostring(record_elem), method="POST" - ) - assert response.status == httplib.CREATED - record = self._to_record(elem=response.object, zone=zone) - - return record - - def update_record(self, record, name=None, type=None, data=None, extra=None): - path = API_ROOT + "hosts/%s.xml" % (record.id) - record_elem = self._to_record_elem(name=name, type=type, data=data, extra=extra) - response = self.connection.request(action=path, data=ET.tostring(record_elem), method="PUT") - assert response.status == httplib.OK - - merged = merge_valid_keys( - params=copy.deepcopy(record.extra), - valid_keys=VALID_RECORD_EXTRA_PARAMS, - extra=extra, - ) - updated_record = get_new_obj( - obj=record, - klass=Record, - attributes={"type": type, "data": data, "extra": merged}, - ) - - return updated_record - - def delete_zone(self, zone): - path = API_ROOT + "zones/%s.xml" % (zone.id) - self.connection.set_context({"resource": "zone", "id": zone.id}) - response = self.connection.request(action=path, method="DELETE") - - return response.status == httplib.OK - - def delete_record(self, record): - path = API_ROOT + "hosts/%s.xml" % (record.id) - self.connection.set_context({"resource": "record", "id": record.id}) - response = self.connection.request(action=path, method="DELETE") - - return response.status == httplib.OK - - def ex_get_zone_by_domain(self, domain): - """ - Retrieve a zone object by the domain name. - - :param domain: The domain which should be used - :type domain: ``str`` - - :rtype: :class:`Zone` - """ - path = API_ROOT + "zones/%s.xml" % (domain) - self.connection.set_context({"resource": "zone", "id": domain}) - data = self.connection.request(path).object - zone = self._to_zone(elem=data) - - return zone - - def ex_force_slave_axfr(self, zone): - """ - Force a zone transfer. - - :param zone: Zone which should be used. - :type zone: :class:`Zone` - - :rtype: :class:`Zone` - """ - path = API_ROOT + "zones/%s/force_slave_axfr.xml" % (zone.id) - self.connection.set_context({"resource": "zone", "id": zone.id}) - response = self.connection.request(path, method="POST") - assert response.status == httplib.ACCEPTED - - return zone - - def _to_zone_elem(self, domain=None, type=None, ttl=None, extra=None): - zone_elem = ET.Element("zone", {}) - - if domain: - domain_elem = ET.SubElement(zone_elem, "domain") - domain_elem.text = domain - - if type: - ns_type_elem = ET.SubElement(zone_elem, "ns-type") - - if type == "master": - ns_type_elem.text = "pri_sec" - elif type == "slave": - if not extra or "ns1" not in extra: - raise LibcloudError( - "ns1 extra attribute is required " + "when zone type is slave", - driver=self, - ) - - ns_type_elem.text = "sec" - ns1_elem = ET.SubElement(zone_elem, "ns1") - ns1_elem.text = extra["ns1"] - elif type == "std_master": - # TODO: Each driver should provide supported zone types - # Slave name servers are elsewhere - - if not extra or "slave-nameservers" not in extra: - raise LibcloudError( - "slave-nameservers extra " - + "attribute is required whenzone " - + "type is std_master", - driver=self, - ) - - ns_type_elem.text = "pri" - slave_nameservers_elem = ET.SubElement(zone_elem, "slave-nameservers") - slave_nameservers_elem.text = extra["slave-nameservers"] - - if ttl: - default_ttl_elem = ET.SubElement(zone_elem, "default-ttl") - default_ttl_elem.text = str(ttl) - - if extra and "tag-list" in extra: - tags = extra["tag-list"] - - tags_elem = ET.SubElement(zone_elem, "tag-list") - tags_elem.text = " ".join(tags) - - return zone_elem - - def _to_record_elem(self, name=None, type=None, data=None, extra=None): - record_elem = ET.Element("host", {}) - - if name: - name_elem = ET.SubElement(record_elem, "hostname") - name_elem.text = name - - if type is not None: - type_elem = ET.SubElement(record_elem, "host-type") - type_elem.text = self.RECORD_TYPE_MAP[type] - - if data: - data_elem = ET.SubElement(record_elem, "data") - data_elem.text = data - - if extra: - if "ttl" in extra: - ttl_elem = ET.SubElement(record_elem, "ttl", {"type": "integer"}) - ttl_elem.text = str(extra["ttl"]) - - if "priority" in extra: - # Only MX and SRV records support priority - priority_elem = ET.SubElement(record_elem, "priority", {"type": "integer"}) - - priority_elem.text = str(extra["priority"]) - - if "notes" in extra: - notes_elem = ET.SubElement(record_elem, "notes") - notes_elem.text = extra["notes"] - - return record_elem - - def _to_zones(self, elem): - zones = [] - - for item in findall(element=elem, xpath="zone"): - zone = self._to_zone(elem=item) - zones.append(zone) - - return zones - - def _to_zone(self, elem): - id = findtext(element=elem, xpath="id") - domain = findtext(element=elem, xpath="domain") - type = findtext(element=elem, xpath="ns-type") - type = "master" if type.find("pri") == 0 else "slave" - ttl = findtext(element=elem, xpath="default-ttl") - - hostmaster = findtext(element=elem, xpath="hostmaster") - custom_ns = findtext(element=elem, xpath="custom-ns") - custom_nameservers = findtext(element=elem, xpath="custom-nameservers") - notes = findtext(element=elem, xpath="notes") - nx_ttl = findtext(element=elem, xpath="nx-ttl") - slave_nameservers = findtext(element=elem, xpath="slave-nameservers") - tags = findtext(element=elem, xpath="tag-list") - tags = tags.split(" ") if tags else [] - - extra = { - "hostmaster": hostmaster, - "custom-ns": custom_ns, - "custom-nameservers": custom_nameservers, - "notes": notes, - "nx-ttl": nx_ttl, - "slave-nameservers": slave_nameservers, - "tags": tags, - } - zone = Zone(id=str(id), domain=domain, type=type, ttl=int(ttl), driver=self, extra=extra) - - return zone - - def _to_records(self, elem, zone): - records = [] - - for item in findall(element=elem, xpath="host"): - record = self._to_record(elem=item, zone=zone) - records.append(record) - - return records - - def _to_record(self, elem, zone): - id = findtext(element=elem, xpath="id") - name = findtext(element=elem, xpath="hostname") - type = findtext(element=elem, xpath="host-type") - type = self._string_to_record_type(type) - data = findtext(element=elem, xpath="data") - - notes = findtext(element=elem, xpath="notes", no_text_value=None) - state = findtext(element=elem, xpath="state", no_text_value=None) - fqdn = findtext(element=elem, xpath="fqdn", no_text_value=None) - priority = findtext(element=elem, xpath="priority", no_text_value=None) - ttl = findtext(element=elem, xpath="ttl", no_text_value=None) - - if not name: - name = None - - if ttl: - ttl = int(ttl) - - extra = { - "notes": notes, - "state": state, - "fqdn": fqdn, - "priority": priority, - "ttl": ttl, - } - - record = Record( - id=id, - name=name, - type=type, - data=data, - zone=zone, - driver=self, - ttl=ttl, - extra=extra, - ) - - return record - - def _get_more(self, rtype, **kwargs): - exhausted = False - last_key = None - - while not exhausted: - items, last_key, exhausted = self._get_data(rtype, last_key, **kwargs) - - yield from items - - def _get_data(self, rtype, last_key, **kwargs): - # Note: last_key in this case really is a "last_page". - # TODO: Update base driver and change last_key to something more - # generic - e.g. marker - params = {} - params["per_page"] = ITEMS_PER_PAGE - params["page"] = last_key + 1 if last_key else 1 - - if rtype == "zones": - path = API_ROOT + "zones.xml" - response = self.connection.request(path) - transform_func = self._to_zones - elif rtype == "records": - zone = kwargs["zone"] - path = API_ROOT + "zones/%s/hosts.xml" % (zone.id) - self.connection.set_context({"resource": "zone", "id": zone.id}) - response = self.connection.request(path, params=params) - transform_func = self._to_records - else: - raise ValueError(f"Unsupported rtype: {rtype}") - - exhausted = False - result_count = int(response.headers.get("x-query-count", 0)) - - if (params["page"] * ITEMS_PER_PAGE) >= result_count: - exhausted = True - - if response.status == httplib.OK: - items = transform_func(elem=response.object, **kwargs) - - return items, params["page"], exhausted - else: - return [], None, True diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index 7469275897..6c6678821f 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -30,7 +30,6 @@ DRIVERS = { Provider.DUMMY: ("libcloud.dns.drivers.dummy", "DummyDNSDriver"), Provider.LINODE: ("libcloud.dns.drivers.linode", "LinodeDNSDriver"), - Provider.ZERIGO: ("libcloud.dns.drivers.zerigo", "ZerigoDNSDriver"), Provider.RACKSPACE: ("libcloud.dns.drivers.rackspace", "RackspaceDNSDriver"), Provider.ROUTE53: ("libcloud.dns.drivers.route53", "Route53DNSDriver"), Provider.GANDI: ("libcloud.dns.drivers.gandi", "GandiDNSDriver"), diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index ec2cb7b45f..32972d3976 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -60,7 +60,6 @@ class Provider: ROUTE53 = "route53" VULTR = "vultr" WORLDWIDEDNS = "worldwidedns" - ZERIGO = "zerigo" ZONOMI = "zonomi" DNSPOD = "dnspod" # Deprecated diff --git a/libcloud/test/dns/fixtures/zerigo/create_record.xml b/libcloud/test/dns/fixtures/zerigo/create_record.xml deleted file mode 100644 index 2a44e00e22..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/create_record.xml +++ /dev/null @@ -1,13 +0,0 @@ - - 2008-12-07T02:51:13Z - 127.0.0.1 - www.example.com - A - www - 23456780 - - - - 2008-12-07T02:51:13Z - 12345678 - diff --git a/libcloud/test/dns/fixtures/zerigo/create_zone.xml b/libcloud/test/dns/fixtures/zerigo/create_zone.xml deleted file mode 100644 index a48156c667..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/create_zone.xml +++ /dev/null @@ -1,18 +0,0 @@ - - 2008-12-07T02:40:02Z - ns1.example.com,ns2.example.com - true - 600 - foo.bar.com - dnsadmin@example.com - 12345679 - - - pri_sec - - - - 2008-12-07T02:40:02Z - 0 - - diff --git a/libcloud/test/dns/fixtures/zerigo/create_zone_validation_error.xml b/libcloud/test/dns/fixtures/zerigo/create_zone_validation_error.xml deleted file mode 100644 index 664b9719ed..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/create_zone_validation_error.xml +++ /dev/null @@ -1,4 +0,0 @@ - - Ns type is not included in the list - Default ttl must be greater than or equal to 60 - diff --git a/libcloud/test/dns/fixtures/zerigo/get_record.xml b/libcloud/test/dns/fixtures/zerigo/get_record.xml deleted file mode 100644 index 31619a8935..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/get_record.xml +++ /dev/null @@ -1,13 +0,0 @@ - - 2008-12-07T02:51:13Z - 172.16.16.1 - example.com - A - www - 23456789 - - - - 2008-12-07T02:51:13Z - 12345678 - diff --git a/libcloud/test/dns/fixtures/zerigo/get_zone.xml b/libcloud/test/dns/fixtures/zerigo/get_zone.xml deleted file mode 100644 index f94522c457..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/get_zone.xml +++ /dev/null @@ -1,32 +0,0 @@ - - 2008-12-07T02:40:02Z - ns1.example.com,ns2.example.com - true - 600 - example.com - dnsadmin@example.com - 12345678 - - - pri_sec - - - one two - 2008-12-07T02:40:02Z - 1 - - - 2008-12-07T02:51:13Z - 172.16.16.1 - example.com - A - - 23456789 - - - - 2008-12-07T02:51:13Z - 12345678 - - - diff --git a/libcloud/test/dns/fixtures/zerigo/list_records.xml b/libcloud/test/dns/fixtures/zerigo/list_records.xml deleted file mode 100644 index f226162c79..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/list_records.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - 2008-12-07T02:51:13Z - 172.16.16.1 - www.example.com - A - www - 23456789 - - - - 2008-12-07T02:51:13Z - 12345678 - - - 2008-12-07T02:51:13Z - 172.16.16.2 - test.example.com - A - test - 23456789 - - - 3600 - 2008-12-07T02:51:13Z - 12345678 - - - 2008-12-07T02:51:13Z - 172.16.16.3 - test2.example.com - A - - 23456789 - - - 3600 - 2008-12-07T02:51:13Z - 12345678 - - - 2008-12-07T02:51:13Z - 172.16.16.4 - test4.example.com - A - 23456789 - - - 3600 - 2008-12-07T02:51:13Z - 12345678 - - - diff --git a/libcloud/test/dns/fixtures/zerigo/list_records_no_results.xml b/libcloud/test/dns/fixtures/zerigo/list_records_no_results.xml deleted file mode 100644 index 7020c67a6e..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/list_records_no_results.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libcloud/test/dns/fixtures/zerigo/list_zones.xml b/libcloud/test/dns/fixtures/zerigo/list_zones.xml deleted file mode 100644 index 3870926e05..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/list_zones.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - 2008-12-07T02:40:02Z - - false - 600 - example.com - - 12345678 - test foo bar - - pri_sec - - - 2008-12-07T02:40:02Z - - diff --git a/libcloud/test/dns/fixtures/zerigo/list_zones_no_results.xml b/libcloud/test/dns/fixtures/zerigo/list_zones_no_results.xml deleted file mode 100644 index 0572fec45c..0000000000 --- a/libcloud/test/dns/fixtures/zerigo/list_zones_no_results.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libcloud/test/dns/test_zerigo.py b/libcloud/test/dns/test_zerigo.py deleted file mode 100644 index 1fc1af8842..0000000000 --- a/libcloud/test/dns/test_zerigo.py +++ /dev/null @@ -1,344 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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 - -import sys -import unittest - -from libcloud.test import MockHttp -from libcloud.dns.types import RecordType, ZoneDoesNotExistError, RecordDoesNotExistError -from libcloud.utils.py3 import httplib -from libcloud.common.types import LibcloudError, InvalidCredsError -from libcloud.test.secrets import DNS_PARAMS_ZERIGO -from libcloud.dns.drivers.zerigo import ZerigoError, ZerigoDNSDriver -from libcloud.test.file_fixtures import DNSFileFixtures - - -class ZerigoTests(unittest.TestCase): - def setUp(self): - ZerigoDNSDriver.connectionCls.conn_class = ZerigoMockHttp - ZerigoMockHttp.type = None - self.driver = ZerigoDNSDriver(*DNS_PARAMS_ZERIGO) - - def test_invalid_credentials(self): - ZerigoMockHttp.type = "INVALID_CREDS" - - try: - list(self.driver.list_zones()) - except InvalidCredsError: - pass - else: - self.fail("Exception was not thrown") - - def test_list_record_types(self): - record_types = self.driver.list_record_types() - self.assertEqual(len(record_types), 13) - self.assertTrue(RecordType.A in record_types) - - def test_list_zones_success(self): - zones = self.driver.list_zones() - self.assertEqual(len(zones), 1) - self.assertEqual(zones[0].domain, "example.com") - self.assertEqual(zones[0].type, "master") - self.assertEqual(zones[0].extra["notes"], "test foo bar") - - def test_list_zones_no_results(self): - ZerigoMockHttp.type = "NO_RESULTS" - zones = self.driver.list_zones() - self.assertEqual(len(zones), 0) - - def test_list_records_success(self): - zone = self.driver.list_zones()[0] - records = list(self.driver.list_records(zone=zone)) - - self.assertEqual(len(records), 4) - self.assertEqual(records[0].name, "www") - self.assertEqual(records[0].type, RecordType.A) - self.assertEqual(records[0].data, "172.16.16.1") - self.assertEqual(records[0].extra["fqdn"], "www.example.com") - self.assertIsNone(records[0].extra["notes"]) - self.assertIsNone(records[0].extra["priority"]) - - self.assertEqual(records[1].name, "test") - self.assertEqual(records[1].extra["ttl"], 3600) - - def test_record_with_empty_name(self): - zone = self.driver.list_zones()[0] - record1 = list(self.driver.list_records(zone=zone))[-1] - record2 = list(self.driver.list_records(zone=zone))[-2] - - self.assertIsNone(record1.name) - self.assertIsNone(record2.name) - - def test_list_records_no_results(self): - zone = self.driver.list_zones()[0] - ZerigoMockHttp.type = "NO_RESULTS" - records = list(self.driver.list_records(zone=zone)) - self.assertEqual(len(records), 0) - - def test_list_records_zone_does_not_exist(self): - zone = self.driver.list_zones()[0] - - ZerigoMockHttp.type = "ZONE_DOES_NOT_EXIST" - try: - list(self.driver.list_records(zone=zone)) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, zone.id) - else: - self.fail("Exception was not thrown") - pass - - def test_get_zone_success(self): - zone = self.driver.get_zone(zone_id=12345678) - - self.assertEqual(zone.id, "12345678") - self.assertEqual(zone.domain, "example.com") - self.assertEqual(zone.extra["hostmaster"], "dnsadmin@example.com") - self.assertEqual(zone.type, "master") - - def test_get_zone_does_not_exist(self): - ZerigoMockHttp.type = "DOES_NOT_EXIST" - - try: - self.driver.get_zone(zone_id="4444") - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, "4444") - else: - self.fail("Exception was not thrown") - - def test_get_record_success(self): - record = self.driver.get_record(zone_id="12345678", record_id="23456789") - self.assertEqual(record.id, "23456789") - self.assertEqual(record.name, "www") - self.assertEqual(record.type, RecordType.A) - - def test_get_record_zone_does_not_exist(self): - ZerigoMockHttp.type = "ZONE_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="444", record_id="28536") - except ZoneDoesNotExistError: - pass - else: - self.fail("Exception was not thrown") - - def test_get_record_record_does_not_exist(self): - ZerigoMockHttp.type = "RECORD_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="12345678", record_id="28536") - except RecordDoesNotExistError: - pass - else: - self.fail("Exception was not thrown") - - def test_create_zone_success(self): - ZerigoMockHttp.type = "CREATE_ZONE" - - zone = self.driver.create_zone(domain="foo.bar.com", type="master", ttl=None, extra=None) - self.assertEqual(zone.id, "12345679") - self.assertEqual(zone.domain, "foo.bar.com") - - def test_create_zone_validaton_error(self): - ZerigoMockHttp.type = "CREATE_ZONE_VALIDATION_ERROR" - - try: - self.driver.create_zone(domain="foo.bar.com", type="master", ttl=10, extra=None) - except ZerigoError as e: - self.assertEqual(len(e.errors), 2) - else: - self.fail("Exception was not thrown") - - def test_update_zone_success(self): - zone = self.driver.list_zones()[0] - updated_zone = self.driver.update_zone(zone=zone, ttl=10, extra={"notes": "bar foo"}) - - self.assertEqual(zone.extra["notes"], "test foo bar") - - self.assertEqual(updated_zone.id, zone.id) - self.assertEqual(updated_zone.domain, "example.com") - self.assertEqual(updated_zone.type, zone.type) - self.assertEqual(updated_zone.ttl, 10) - self.assertEqual(updated_zone.extra["notes"], "bar foo") - - def test_update_zone_domain_cannot_be_changed(self): - zone = self.driver.list_zones()[0] - - try: - self.driver.update_zone(zone=zone, domain="libcloud.org") - except LibcloudError: - pass - else: - self.fail("Exception was not thrown") - - def test_create_record_success(self): - zone = self.driver.list_zones()[0] - - ZerigoMockHttp.type = "CREATE_RECORD" - record = self.driver.create_record( - name="www", zone=zone, type=RecordType.A, data="127.0.0.1" - ) - - self.assertEqual(record.id, "23456780") - self.assertEqual(record.name, "www") - self.assertEqual(record.zone, zone) - self.assertEqual(record.type, RecordType.A) - self.assertEqual(record.data, "127.0.0.1") - - def test_update_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - updated_record = self.driver.update_record( - record=record, name="www", type=RecordType.AAAA, data="::1" - ) - - self.assertEqual(record.data, "172.16.16.1") - - self.assertEqual(updated_record.id, record.id) - self.assertEqual(updated_record.name, "www") - self.assertEqual(updated_record.zone, record.zone) - self.assertEqual(updated_record.type, RecordType.AAAA) - self.assertEqual(updated_record.data, "::1") - - def test_delete_zone_success(self): - zone = self.driver.list_zones()[0] - status = self.driver.delete_zone(zone=zone) - self.assertTrue(status) - - def test_delete_zone_does_not_exist(self): - zone = self.driver.list_zones()[0] - - ZerigoMockHttp.type = "ZONE_DOES_NOT_EXIST" - - try: - self.driver.delete_zone(zone=zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, zone.id) - else: - self.fail("Exception was not thrown") - - def test_delete_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - status = self.driver.delete_record(record=record) - self.assertTrue(status) - - def test_delete_record_does_not_exist(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - - ZerigoMockHttp.type = "RECORD_DOES_NOT_EXIST" - - try: - self.driver.delete_record(record=record) - except RecordDoesNotExistError as e: - self.assertEqual(e.record_id, record.id) - else: - self.fail("Exception was not thrown") - - -class ZerigoMockHttp(MockHttp): - fixtures = DNSFileFixtures("zerigo") - - def _api_1_1_zones_xml_INVALID_CREDS(self, method, url, body, headers): - body = "HTTP Basic: Access denied.\n" - return (httplib.UNAUTHORIZED, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_xml(self, method, url, body, headers): - body = self.fixtures.load("list_zones.xml") - return (httplib.OK, body, {"x-query-count": "1"}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_xml_NO_RESULTS(self, method, url, body, headers): - body = self.fixtures.load("list_zones_no_results.xml") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_hosts_xml(self, method, url, body, headers): - body = self.fixtures.load("list_records.xml") - return (httplib.OK, body, {"x-query-count": "1"}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_hosts_xml_NO_RESULTS(self, method, url, body, headers): - body = self.fixtures.load("list_records_no_results.xml") - return (httplib.OK, body, {"x-query-count": "0"}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_hosts_xml_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_xml(self, method, url, body, headers): - body = self.fixtures.load("get_zone.xml") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_4444_xml_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_hosts_23456789_xml(self, method, url, body, headers): - body = self.fixtures.load("get_record.xml") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_444_xml_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_xml_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("get_zone.xml") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_hosts_28536_xml_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_xml_CREATE_ZONE(self, method, url, body, headers): - body = self.fixtures.load("create_zone.xml") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_xml_CREATE_ZONE_VALIDATION_ERROR(self, method, url, body, headers): - body = self.fixtures.load("create_zone_validation_error.xml") - return (httplib.UNPROCESSABLE_ENTITY, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_hosts_xml_CREATE_RECORD(self, method, url, body, headers): - body = self.fixtures.load("create_record.xml") - return (httplib.CREATED, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_zones_12345678_xml_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.OK]) - - def _api_1_1_hosts_23456789_xml_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.OK]) - - """ - def (self, method, url, body, headers): - body = self.fixtures.load('.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def (self, method, url, body, headers): - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def (self, method, url, body, headers): - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def (self, method, url, body, headers): - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def (self, method, url, body, headers): - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def (self, method, url, body, headers): - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - """ - - -if __name__ == "__main__": - sys.exit(unittest.main()) From 26cc840b3c99ba52bde9ba48839d04c9310f71ff Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 27 Mar 2025 10:33:42 -0500 Subject: [PATCH 02/52] Deprecate DNSimple DNS driver The DNSimple v1 API was shutdown on May 31, 2018: https://blog.dnsimple.com/2018/03/api-v1-shutdown-notice/ --- libcloud/dns/drivers/dnsimple.py | 2 ++ libcloud/dns/providers.py | 2 +- libcloud/test/dns/test_dnsimple.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libcloud/dns/drivers/dnsimple.py b/libcloud/dns/drivers/dnsimple.py index 11bb5d5848..68c8208923 100644 --- a/libcloud/dns/drivers/dnsimple.py +++ b/libcloud/dns/drivers/dnsimple.py @@ -16,6 +16,8 @@ DNSimple DNS Driver """ +# FIXME: v1 API was deprecated on May 31, 2018 + __all__ = ["DNSimpleDNSDriver"] try: diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index 6c6678821f..2db92975da 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -40,7 +40,6 @@ "DigitalOceanDNSDriver", ), Provider.WORLDWIDEDNS: ("libcloud.dns.drivers.worldwidedns", "WorldWideDNSDriver"), - Provider.DNSIMPLE: ("libcloud.dns.drivers.dnsimple", "DNSimpleDNSDriver"), Provider.POINTDNS: ("libcloud.dns.drivers.pointdns", "PointDNSDriver"), Provider.VULTR: ("libcloud.dns.drivers.vultr", "VultrDNSDriver"), Provider.LIQUIDWEB: ("libcloud.dns.drivers.liquidweb", "LiquidWebDNSDriver"), @@ -57,6 +56,7 @@ Provider.ONAPP: ("libcloud.dns.drivers.onapp", "OnAppDNSDriver"), Provider.RCODEZERO: ("libcloud.dns.drivers.rcodezero", "RcodeZeroDNSDriver"), # Deprecated + Provider.DNSIMPLE: ("libcloud.dns.drivers.dnsimple", "DNSimpleDNSDriver"), Provider.RACKSPACE_US: ("libcloud.dns.drivers.rackspace", "RackspaceUSDNSDriver"), Provider.RACKSPACE_UK: ("libcloud.dns.drivers.rackspace", "RackspaceUKDNSDriver"), } diff --git a/libcloud/test/dns/test_dnsimple.py b/libcloud/test/dns/test_dnsimple.py index 09d1d5c9fc..e483ebfacf 100644 --- a/libcloud/test/dns/test_dnsimple.py +++ b/libcloud/test/dns/test_dnsimple.py @@ -24,6 +24,7 @@ from libcloud.dns.drivers.dnsimple import DNSimpleDNSDriver +@unittest.skip("v1 API is deprecated") class DNSimpleDNSTests(unittest.TestCase): def setUp(self): DNSimpleDNSDriver.connectionCls.conn_class = DNSimpleDNSMockHttp From 8b6b835815a4a97089fdcff006acbcdd177f1463 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 15:51:16 -0500 Subject: [PATCH 03/52] Deprecate RcodeZero DNS driver The RcodeZero v1 API has been shutdown: https://my.rcodezero.at/openapi/ --- libcloud/dns/drivers/rcodezero.py | 2 ++ libcloud/dns/providers.py | 2 +- libcloud/test/dns/test_rcodezero.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libcloud/dns/drivers/rcodezero.py b/libcloud/dns/drivers/rcodezero.py index 21a79a87ee..8f5829e60c 100644 --- a/libcloud/dns/drivers/rcodezero.py +++ b/libcloud/dns/drivers/rcodezero.py @@ -31,6 +31,8 @@ "RcodeZeroDNSDriver", ] +# FIXME: v1 API is deprecated + class RcodeZeroResponse(JsonResponse): def success(self): diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index 2db92975da..1a33020250 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -54,11 +54,11 @@ Provider.BUDDYNS: ("libcloud.dns.drivers.buddyns", "BuddyNSDNSDriver"), Provider.POWERDNS: ("libcloud.dns.drivers.powerdns", "PowerDNSDriver"), Provider.ONAPP: ("libcloud.dns.drivers.onapp", "OnAppDNSDriver"), - Provider.RCODEZERO: ("libcloud.dns.drivers.rcodezero", "RcodeZeroDNSDriver"), # Deprecated Provider.DNSIMPLE: ("libcloud.dns.drivers.dnsimple", "DNSimpleDNSDriver"), Provider.RACKSPACE_US: ("libcloud.dns.drivers.rackspace", "RackspaceUSDNSDriver"), Provider.RACKSPACE_UK: ("libcloud.dns.drivers.rackspace", "RackspaceUKDNSDriver"), + Provider.RCODEZERO: ("libcloud.dns.drivers.rcodezero", "RcodeZeroDNSDriver"), } diff --git a/libcloud/test/dns/test_rcodezero.py b/libcloud/test/dns/test_rcodezero.py index a77a0cf4d8..58175f41ef 100644 --- a/libcloud/test/dns/test_rcodezero.py +++ b/libcloud/test/dns/test_rcodezero.py @@ -24,6 +24,7 @@ from libcloud.dns.drivers.rcodezero import RcodeZeroDNSDriver +@unittest.skip("v1 API is deprecated") class RcodeZeroDNSTestCase(LibcloudTestCase): def setUp(self): RcodeZeroDNSDriver.connectionCls.conn_class = RcodeZeroDNSMockHttp From d3581aba6d216e59526ce2deb3b3919acbe09fb4 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 26 Mar 2025 19:43:42 -0500 Subject: [PATCH 04/52] test: Add optional request history to MockHttp Enable unit tests to verify HTTP requests they submit. --- libcloud/test/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/libcloud/test/__init__.py b/libcloud/test/__init__.py index 1cc595f685..21e4c28304 100644 --- a/libcloud/test/__init__.py +++ b/libcloud/test/__init__.py @@ -14,11 +14,13 @@ # limitations under the License. import os +import json import random import unittest import requests import requests_mock +from requests.structures import CaseInsensitiveDict from libcloud.http import LibcloudConnection from libcloud.utils.py3 import PY2, httplib, parse_qs, urlparse, urlquote, parse_qsl @@ -87,6 +89,19 @@ def read(self, chunk_size=None): return StringIO.read(self) +class MockRequest: + def __init__(self, method, url, query, body, headers): + self.method = method + self.url = url + self.query = parse_qs(query) + self.headers = CaseInsensitiveDict(headers) + self.body = body + + @property + def json(self): + return json.loads(self.body) + + class MockHttp(LibcloudConnection, unittest.TestCase): """ A mock HTTP client/server suitable for testing purposes. This replaces @@ -102,6 +117,8 @@ class MockHttp(LibcloudConnection, unittest.TestCase): use_param = None # will use this param to namespace the request function test = None # TestCase instance which is using this mock proxy_url = None + keep_history = False + history = [] def __init__(self, *args, **kwargs): # Load assertion methods into the class, in case people want to assert @@ -118,6 +135,10 @@ def _get_request(self, method, url, body=None, headers=None): # Find a method we can use for this request parsed = urlparse.urlparse(url) _, _, path, _, query, _ = parsed + + if self.keep_history: + self.history.append(MockRequest(method, path, query, body, headers)) + qs = parse_qs(query) if path.endswith("/"): path = path[:-1] From d5297aceaf295ec5af219c16644f307ef899a176 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 26 Mar 2025 19:50:08 -0500 Subject: [PATCH 05/52] dns: buddyns: Add API request checking --- libcloud/test/dns/test_buddyns.py | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/libcloud/test/dns/test_buddyns.py b/libcloud/test/dns/test_buddyns.py index 4506e99965..55e2e8c950 100644 --- a/libcloud/test/dns/test_buddyns.py +++ b/libcloud/test/dns/test_buddyns.py @@ -28,6 +28,7 @@ class BuddyNSDNSTests(unittest.TestCase): def setUp(self): BuddyNSMockHttp.type = None + BuddyNSMockHttp.history.clear() BuddyNSDNSDriver.connectionCls.conn_class = BuddyNSMockHttp self.driver = BuddyNSDNSDriver(*DNS_PARAMS_BUDDYNS) self.test_zone = Zone( @@ -43,12 +44,24 @@ def test_list_zones_empty(self): BuddyNSMockHttp.type = "EMPTY_ZONES_LIST" zones = self.driver.list_zones() + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v2/zone/") + self.assertEqual(zones, []) def test_list_zones_success(self): BuddyNSMockHttp.type = "LIST_ZONES" zones = self.driver.list_zones() + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v2/zone/") + self.assertEqual(len(zones), 2) zone = zones[0] @@ -73,12 +86,24 @@ def test_delete_zone_zone_does_not_exist(self): else: self.fail("Exception was not thrown") + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/api/v2/zone/{self.test_zone.domain}") + def test_delete_zone_success(self): BuddyNSMockHttp.type = "DELETE_ZONE_SUCCESS" status = self.driver.delete_zone(zone=self.test_zone) self.assertTrue(status) + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/api/v2/zone/{self.test_zone.domain}") + def test_get_zone_zone_does_not_exist(self): BuddyNSMockHttp.type = "GET_ZONE_ZONE_DOES_NOT_EXIST" try: @@ -88,10 +113,22 @@ def test_get_zone_zone_does_not_exist(self): else: self.fail("Exception was not thrown") + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v2/zone/zonedoesnotexist.com") + def test_get_zone_success(self): BuddyNSMockHttp.type = "GET_ZONE_SUCCESS" zone = self.driver.get_zone(zone_id="myexample.com") + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v2/zone/myexample.com") + self.assertEqual(zone.id, "myexample.com") self.assertEqual(zone.domain, "myexample.com") self.assertIsNone(zone.type) @@ -102,6 +139,13 @@ def test_create_zone_success(self): BuddyNSMockHttp.type = "CREATE_ZONE_SUCCESS" zone = self.driver.create_zone(domain="microsoft.com") + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/api/v2/zone/") + self.assertEqual(sent.json["name"], "microsoft.com") + self.assertEqual(zone.id, "microsoft.com") self.assertEqual(zone.domain, "microsoft.com") self.assertIsNone(zone.type), @@ -117,9 +161,18 @@ def test_create_zone_zone_already_exists(self): else: self.fail("Exception was not thrown") + reqs = BuddyNSMockHttp.history + self.assertEqual(len(reqs), 1) + sent = reqs.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/api/v2/zone/") + self.assertEqual(sent.json["name"], "newzone.com") + self.assertEqual(sent.json["master"], "13.0.0.1") + class BuddyNSMockHttp(MockHttp): fixtures = DNSFileFixtures("buddyns") + keep_history = True def _api_v2_zone_EMPTY_ZONES_LIST(self, method, url, body, headers): body = self.fixtures.load("empty_zones_list.json") From dde99d34a023fa07923178b747ed92d34180fae5 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 26 Mar 2025 21:08:01 -0500 Subject: [PATCH 06/52] dns: cloudflare: Add API request checking --- libcloud/test/dns/test_cloudflare.py | 80 ++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/libcloud/test/dns/test_cloudflare.py b/libcloud/test/dns/test_cloudflare.py index 87da31ff9a..b71ee7f790 100644 --- a/libcloud/test/dns/test_cloudflare.py +++ b/libcloud/test/dns/test_cloudflare.py @@ -39,6 +39,7 @@ def setUp(self): CloudFlareDNSDriver.MEMBERSHIPS_PAGE_SIZE = 5 CloudFlareMockHttp.type = None CloudFlareMockHttp.use_param = "a" + CloudFlareMockHttp.history.clear() self.driver = CloudFlareDNSDriver(*DNS_PARAMS_CLOUDFLARE) def test_auth_key(self): @@ -56,6 +57,11 @@ def test_list_record_types(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url.rsplit("?")[0], "/client/v4/zones") + self.assertEqual(len(zones), 1) zone = zones[0] @@ -69,6 +75,10 @@ def test_list_zones(self): def test_get_record(self): record = self.driver.get_record("1234", "364797364") + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/client/v4/zones/1234/dns_records/364797364") + self.assertEqual(record.id, "364797364") self.assertIsNone(record.name) self.assertEqual(record.type, "A") @@ -85,6 +95,11 @@ def test_get_record_record_is_invalid(self): def test_list_records(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url.rsplit("?")[0], f"/client/v4/zones/{zone.id}/dns_records") + self.assertEqual(len(records), 11) record = records[0] @@ -121,6 +136,11 @@ def test_list_records(self): def test_get_zone(self): zone = self.driver.get_zone(zone_id="1234") + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/client/v4/zones/1234") + self.assertEqual(zone.id, "1234") self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.type, "master") @@ -142,6 +162,15 @@ def test_create_record(self): data="127.0.0.3", extra={"proxied": True}, ) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}/dns_records") + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["name"], "test5") + self.assertEqual(sent.json["content"], "127.0.0.3") + self.assertEqual(sent.json["proxied"], True) + self.assertEqual(record.id, "412561327") self.assertEqual(record.name, "test5") self.assertEqual(record.type, "A") @@ -154,6 +183,17 @@ def test_create_record_SSHFP_record_type(self): record = self.driver.create_record( name="test_sshfp", zone=zone, type=RecordType.SSHFP, data="2 1 ABCDEF12345" ) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}/dns_records") + self.assertEqual(sent.json["type"], "SSHFP") + self.assertEqual(sent.json["name"], "test_sshfp") + self.assertEqual(sent.json["content"], None) + self.assertEqual( + sent.json["data"], {"algorithm": "2", "type": "1", "fingerprint": "ABCDEF12345"} + ) + self.assertEqual(record.id, "200") self.assertEqual(record.name, "test_sshfp") self.assertEqual(record.type, "SSHFP") @@ -166,6 +206,14 @@ def test_create_record_CAA_record_type(self): record = self.driver.create_record( name="test5", zone=zone, type=RecordType.CAA, data="0 issue caa.example.com" ) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}/dns_records") + self.assertEqual(sent.json["type"], "CAA") + self.assertEqual(sent.json["name"], "test5") + self.assertEqual(sent.json["content"], "0\tissue\tcaa.example.com") + self.assertEqual(record.id, "412561327") self.assertEqual(record.name, "test5") self.assertEqual(record.type, "A") @@ -214,6 +262,14 @@ def test_update_record(self): extra={"proxied": True}, ) + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}/dns_records/{record.id}") + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["name"], "test6") + self.assertEqual(sent.json["content"], "127.0.0.4") + self.assertEqual(sent.json["extra"]["proxied"], True) + self.assertEqual(updated_record.name, "test6") self.assertEqual(updated_record.type, "A") self.assertEqual(updated_record.data, "127.0.0.4") @@ -233,15 +289,33 @@ def test_delete_record(self): zone = self.driver.list_zones()[0] record = zone.list_records()[0] result = self.driver.delete_record(record=record) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}/dns_records/{record.id}") + self.assertTrue(result) def test_delete_zone(self): zone = self.driver.list_zones()[0] result = self.driver.delete_zone(zone=zone) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}") + self.assertTrue(result) def test_create_zone(self): zone = self.driver.create_zone(domain="example2.com", extra={"jump_start": False}) + + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, f"/client/v4/zones") + self.assertEqual(sent.json["type"], "full") + self.assertEqual(sent.json["name"], "example2.com") + self.assertEqual(sent.json["jump_start"], False) + self.assertEqual(zone.id, "6789") self.assertEqual(zone.domain, "example2.com") @@ -257,6 +331,11 @@ def test_update_zone(self): updated_zone = self.driver.update_zone(zone=zone, domain="", extra={"paused": True}) + sent = CloudFlareMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, f"/client/v4/zones/{zone.id}") + self.assertEqual(sent.json["paused"], True) + self.assertEqual(zone.id, updated_zone.id) self.assertEqual(zone.domain, updated_zone.domain) self.assertEqual(zone.type, updated_zone.type) @@ -312,6 +391,7 @@ def test_normalize_record_data_from_apu(self): class CloudFlareMockHttp(MockHttp, unittest.TestCase): fixtures = DNSFileFixtures("cloudflare") + keep_history = True def _client_v4_memberships(self, method, url, body, headers): if method not in {"GET"}: From 72c39999796cd8890173ebce90da1ec08545dab6 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 3 Apr 2025 14:48:49 -0500 Subject: [PATCH 07/52] fixup: cloudflare: f-string --- libcloud/test/dns/test_cloudflare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libcloud/test/dns/test_cloudflare.py b/libcloud/test/dns/test_cloudflare.py index b71ee7f790..cd1b2cb42f 100644 --- a/libcloud/test/dns/test_cloudflare.py +++ b/libcloud/test/dns/test_cloudflare.py @@ -311,7 +311,7 @@ def test_create_zone(self): sent = CloudFlareMockHttp.history.pop() self.assertEqual(sent.method, "POST") - self.assertEqual(sent.url, f"/client/v4/zones") + self.assertEqual(sent.url, "/client/v4/zones") self.assertEqual(sent.json["type"], "full") self.assertEqual(sent.json["name"], "example2.com") self.assertEqual(sent.json["jump_start"], False) From d0aee37c6177ae15a62ae143b750a5d902d10fa2 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 27 Mar 2025 10:21:29 -0500 Subject: [PATCH 08/52] dns: digitalocean: Add API request checking --- libcloud/test/dns/test_digitalocean.py | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/libcloud/test/dns/test_digitalocean.py b/libcloud/test/dns/test_digitalocean.py index 2b2118b385..3a4c943943 100644 --- a/libcloud/test/dns/test_digitalocean.py +++ b/libcloud/test/dns/test_digitalocean.py @@ -31,18 +31,30 @@ def setUp(self): DigitalOcean_v2_BaseDriver.connectionCls.conn_class = DigitalOceanDNSMockHttp DigitalOceanDNSDriver.connectionCls.conn_class = DigitalOceanDNSMockHttp DigitalOceanDNSMockHttp.type = None + DigitalOceanDNSMockHttp.history.clear() self.driver = DigitalOceanDNSDriver(*DIGITALOCEAN_v2_PARAMS) def tearDown(self): LibcloudConnection.type = None DigitalOceanDNSMockHttp.type = None + DigitalOceanDNSMockHttp.history.clear() def test_list_zones(self): zones = self.driver.list_zones() + + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains") + self.assertTrue(len(zones) >= 1) def test_get_zone(self): zone = self.driver.get_zone("testdomain") + + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains/testdomain") + self.assertEqual(zone.id, "testdomain") def test_get_zone_not_found(self): @@ -52,12 +64,24 @@ def test_get_zone_not_found(self): def test_list_records(self): zone = self.driver.get_zone("testdomain") records = self.driver.list_records(zone) + + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains/testdomain/records") + self.assertTrue(len(records) >= 1) self.assertEqual(records[1].ttl, 1800) self.assertEqual(records[4].ttl, None) def test_get_record(self): record = self.driver.get_record("testdomain", "1234564") + + # [0] '/v2/domains/testdomain/records/1234564' + # [1] '/v2/domains/testdomain' + sent = DigitalOceanDNSMockHttp.history[0] + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains/testdomain/records/1234564") + self.assertEqual(record.id, "1234564") self.assertEqual(record.type, RecordType.A) self.assertEqual(record.data, "123.45.67.89") @@ -70,6 +94,12 @@ def test_get_record_not_found(self): def test_create_zone(self): DigitalOceanDNSMockHttp.type = "CREATE" zone = self.driver.create_zone("testdomain") + + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v2/domains") + self.assertEqual(sent.json["name"], "testdomain") + self.assertEqual(zone.id, "testdomain") def test_create_record(self): @@ -79,6 +109,15 @@ def test_create_record(self): record = self.driver.create_record( "sub", zone, RecordType.A, "234.56.78.90", extra={"ttl": 60} ) + + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, f"/v2/domains/{zone.domain}/records") + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["name"], "sub") + self.assertEqual(sent.json["data"], "234.56.78.90") + self.assertEqual(sent.json["ttl"], 60) + self.assertEqual(record.id, "1234565") self.assertEqual(record.type, RecordType.A) self.assertEqual(record.data, "234.56.78.90") @@ -89,6 +128,15 @@ def test_update_record(self): DigitalOceanDNSMockHttp.type = "UPDATE" record = self.driver.update_record(record, data="234.56.78.90", extra={"ttl": 60}) + + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertIn(sent.method, {"PATCH", "PUT"}) + self.assertEqual(sent.url, f"/v2/domains/testdomain/records/{record.id}") + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["name"], "@") + self.assertEqual(sent.json["data"], "234.56.78.90") + self.assertEqual(sent.json["ttl"], 60) + self.assertEqual(record.id, "1234564") self.assertEqual(record.data, "234.56.78.90") self.assertEqual(record.ttl, 60) @@ -99,15 +147,24 @@ def test_delete_zone(self): DigitalOceanDNSMockHttp.type = "DELETE" self.assertTrue(self.driver.delete_zone(zone)) + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/v2/domains/{zone.domain}") + def test_delete_record(self): record = self.driver.get_record("testdomain", "1234564") DigitalOceanDNSMockHttp.type = "DELETE" self.assertTrue(self.driver.delete_record(record)) + sent = DigitalOceanDNSMockHttp.history.pop() + self.assertIn(sent.method, "DELETE") + self.assertEqual(sent.url, f"/v2/domains/testdomain/records/{record.id}") + class DigitalOceanDNSMockHttp(MockHttp): fixtures = DNSFileFixtures("digitalocean") + keep_history = True response_map = { None: httplib.OK, From 20fb38f621d9a3a207447507c9283f01152aa0f3 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 27 Mar 2025 11:35:14 -0500 Subject: [PATCH 09/52] dns: dnspod: Add API request checking * Fix test_create_zone_success * Fix test_get_record_success --- libcloud/test/dns/test_dnspod.py | 58 ++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/libcloud/test/dns/test_dnspod.py b/libcloud/test/dns/test_dnspod.py index bbbf6f07f4..f7ad2c1c2c 100644 --- a/libcloud/test/dns/test_dnspod.py +++ b/libcloud/test/dns/test_dnspod.py @@ -25,7 +25,7 @@ RecordDoesNotExistError, RecordAlreadyExistsError, ) -from libcloud.utils.py3 import httplib +from libcloud.utils.py3 import httplib, parse_qs from libcloud.test.secrets import DNS_PARAMS_DNSPOD from libcloud.dns.drivers.dnspod import DNSPodDNSDriver from libcloud.test.file_fixtures import DNSFileFixtures @@ -34,6 +34,7 @@ class DNSPodDNSTests(unittest.TestCase): def setUp(self): DNSPodMockHttp.type = None + DNSPodMockHttp.history.clear() DNSPodDNSDriver.connectionCls.conn_class = DNSPodMockHttp self.driver = DNSPodDNSDriver(*DNS_PARAMS_DNSPOD) self.test_zone = Zone( @@ -67,6 +68,10 @@ def test_list_zones_success(self): DNSPodMockHttp.type = "LIST_ZONES" zones = self.driver.list_zones() + sent = DNSPodMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Domain.List") + self.assertEqual(len(zones), 1) zone = zones[0] @@ -89,6 +94,12 @@ def test_get_zone_success(self): DNSPodMockHttp.type = "GET_ZONE_SUCCESS" zone = self.driver.get_zone(zone_id="6") + sent = DNSPodMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Domain.Info") + data = parse_qs(sent.body) + self.assertIn("6", data["domain_id"]) + self.assertEqual(zone.id, "6") self.assertEqual(zone.domain, "dnspod.com") self.assertIsNone(zone.type) @@ -100,6 +111,12 @@ def test_delete_zone_success(self): zone = self.test_zone status = self.driver.delete_zone(zone=zone) + sent = DNSPodMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Domain.Remove") + data = parse_qs(sent.body) + self.assertIn(zone.id, data["domain_id"]) + self.assertEqual(status, True) def test_delete_zone_zone_does_not_exist(self): @@ -114,7 +131,13 @@ def test_delete_zone_zone_does_not_exist(self): def test_create_zone_success(self): DNSPodMockHttp.type = "CREATE_ZONE_SUCCESS" - zone = self.driver.create_zone(domain="example.org") + zone = self.driver.create_zone(domain="api2.com") + + sent = DNSPodMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Domain.Create") + data = parse_qs(sent.body) + self.assertIn("api2.com", data["domain"]) self.assertEqual(zone.id, "3") self.assertEqual(zone.domain, "api2.com") @@ -135,6 +158,13 @@ def test_list_records_success(self): DNSPodMockHttp.type = "LIST_RECORDS_SUCCESS" zone = self.test_zone records = self.driver.list_records(zone=zone) + + sent = DNSPodMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Record.List") + data = parse_qs(sent.body) + self.assertIn(zone.id, data["domain_id"]) + first_record = records[0] self.assertEqual(len(records), 5) @@ -145,7 +175,14 @@ def test_list_records_success(self): def test_get_record_success(self): DNSPodMockHttp.type = "GET_RECORD_SUCCESS" - record = self.driver.get_record(zone_id="31", record_id="31") + record = self.driver.get_record(zone_id="6", record_id="50") + + sent = DNSPodMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Record.Info") + body = parse_qs(sent.body) + self.assertIn("6", body["domain_id"]) + self.assertIn("50", body["record_id"]) self.assertEqual(record.id, "50") self.assertEqual(record.type, "A") @@ -178,6 +215,20 @@ def test_create_record_success(self): data="96.126.115.73", extra={"ttl": 13, "record_line": "default"}, ) + + # [0] /Record.Create + # [1] /Record.Info + sent = DNSPodMockHttp.history[0] + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/Record.Create") + data = parse_qs(sent.body) + self.assertIn(self.test_zone.id, data["domain_id"]) + self.assertIn("A", data["record_type"]) + self.assertIn("@", data["sub_domain"]) + self.assertIn("96.126.115.73", data["value"]) + self.assertIn("13", data["ttl"]) + self.assertIn("default", data["record_line"]) + self.assertEqual(record.id, "50") self.assertEqual(record.name, "@") self.assertEqual(record.data, "96.126.115.73") @@ -201,6 +252,7 @@ def test_create_record_already_exists_error(self): class DNSPodMockHttp(MockHttp): fixtures = DNSFileFixtures("dnspod") + keep_history = True def _Domain_List_EMPTY_ZONES_LIST(self, method, url, body, headers): body = self.fixtures.load("empty_zones_list.json") From f6dd97dbfc3eb3b5b6f65bccbf7a8bb5c2a6e536 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 27 Mar 2025 12:16:16 -0500 Subject: [PATCH 10/52] dns: durabledns: Add API request checking --- libcloud/test/dns/test_durabledns.py | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/libcloud/test/dns/test_durabledns.py b/libcloud/test/dns/test_durabledns.py index c98768dfe4..1e77375c3d 100644 --- a/libcloud/test/dns/test_durabledns.py +++ b/libcloud/test/dns/test_durabledns.py @@ -39,6 +39,7 @@ class DurableDNSTests(LibcloudTestCase): def setUp(self): DurableDNSDriver.connectionCls.conn_class = DurableDNSMockHttp DurableDNSMockHttp.type = None + DurableDNSMockHttp.history.clear() self.driver = DurableDNSDriver(*DNS_PARAMS_DURABLEDNS) def assertHasKeys(self, dictionary, keys): @@ -81,6 +82,11 @@ def test_list_zones(self): ) self.driver.get_zone = MagicMock(return_value=zone) zones = self.driver.list_zones() + + sent = DurableDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/services/dns/listZones.php") + self.assertEqual(len(zones), 2) zone = zones[0] self.assertEqual(zone.id, "myzone.com.") @@ -128,6 +134,13 @@ def test_list_records(self): ) self.driver.get_record = MagicMock(return_value=record) records = self.driver.list_records(zone=zone) + + sent = DurableDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/services/dns/listRecords.php") + data = sent.body.decode() + self.assertIn(":zonename>myzone.com.myzone.com.myzone.com.deletedzone.com.myzone.com.record14500myzone.com.myzone.com.353286987 Date: Thu, 27 Mar 2025 13:39:28 -0500 Subject: [PATCH 11/52] dns: gandi: Add API request checking * Fix test_update_zone --- .../test/dns/fixtures/gandi/update_zone.xml | 43 +++++++ libcloud/test/dns/test_gandi.py | 105 +++++++++++++++++- 2 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 libcloud/test/dns/fixtures/gandi/update_zone.xml diff --git a/libcloud/test/dns/fixtures/gandi/update_zone.xml b/libcloud/test/dns/fixtures/gandi/update_zone.xml new file mode 100644 index 0000000000..da7588b6fc --- /dev/null +++ b/libcloud/test/dns/fixtures/gandi/update_zone.xml @@ -0,0 +1,43 @@ + + + + + + + + date_updated + 20101028T12:38:17 + + + domains + 0 + + + id + 47234 + + + name + other.com + + + owner + AB3917-GANDI + + + public + 0 + + + version + 1 + + + versions + + + + + + + diff --git a/libcloud/test/dns/test_gandi.py b/libcloud/test/dns/test_gandi.py index b99ce1e465..8e21c3b7f4 100644 --- a/libcloud/test/dns/test_gandi.py +++ b/libcloud/test/dns/test_gandi.py @@ -28,6 +28,7 @@ class GandiTests(unittest.TestCase): def setUp(self): GandiDNSDriver.connectionCls.conn_class = GandiMockHttp GandiMockHttp.type = None + GandiMockHttp.history.clear() self.driver = GandiDNSDriver(*DNS_GANDI) def test_list_record_types(self): @@ -37,6 +38,12 @@ def test_list_record_types(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = GandiMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/xmlrpc/") + self.assertIn("domain.zone.list/n<", "><") + self.assertIn("domain.zone.record.list47234/n<", "><") + self.assertIn("domain.zone.info47234/n<", "><") + self.assertIn("domain.zone.createt.com/n<", "><") + self.assertIn("domain.zone.update47234other.com/n<", "><") + self.assertIn("domain.zone.record.add{zone.id}www127.0.0.130/n<", "><") + self.assertIn("domain.zone.record.add{zone.id}wwwA127.0.0.130/n<", "><") + self.assertIn("domain.zone.delete{zone.id}/n<", "><") + self.assertIn("domain.zone.record.delete{zone.id}1{record.name} Date: Thu, 27 Mar 2025 14:36:44 -0500 Subject: [PATCH 12/52] dns: godaddy: Add API request checking --- libcloud/test/dns/test_godaddy.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/libcloud/test/dns/test_godaddy.py b/libcloud/test/dns/test_godaddy.py index 0dcfa87478..bb222eab11 100644 --- a/libcloud/test/dns/test_godaddy.py +++ b/libcloud/test/dns/test_godaddy.py @@ -27,6 +27,7 @@ class GoDaddyTests(unittest.TestCase): def setUp(self): GoDaddyMockHttp.type = None + GoDaddyMockHttp.history.clear() GoDaddyDNSDriver.connectionCls.conn_class = GoDaddyMockHttp self.driver = GoDaddyDNSDriver(*DNS_PARAMS_GODADDY) @@ -36,6 +37,11 @@ def assertHasKeys(self, dictionary, keys): def test_list_zones(self): zones = self.driver.list_zones() + + sent = GoDaddyMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/domains/") + self.assertEqual(len(zones), 5) self.assertEqual(zones[0].id, "177184419") self.assertEqual(zones[0].domain, "aperture-platform.com") @@ -75,6 +81,11 @@ def test_list_records(self): driver=self.driver, ) records = self.driver.list_records(zone) + + sent = GoDaddyMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/domains/aperture-platform.com/records") + self.assertEqual(len(records), 14) self.assertEqual(records[0].type, RecordType.A) self.assertEqual(records[0].name, "@") @@ -83,6 +94,13 @@ def test_list_records(self): def test_get_record(self): record = self.driver.get_record("aperture-platform.com", "www:A") + + # [0] /v1/domains/aperture-platform.com/records/A/www + # [1] /v1/domains/aperture-platform.com/ + sent = GoDaddyMockHttp.history[0] + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/domains/aperture-platform.com/records/A/www") + self.assertEqual(record.id, "www:A") self.assertEqual(record.name, "www") self.assertEqual(record.type, RecordType.A) @@ -99,6 +117,15 @@ def test_create_record(self): record = self.driver.create_record( zone=zone, name="www", type=RecordType.A, data="50.63.202.42" ) + + sent = GoDaddyMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, "/v1/domains/aperture-platform.com/records") + data = sent.json[0] + self.assertEqual(data["name"], "www") + self.assertEqual(data["type"], "A") + self.assertEqual(data["data"], "50.63.202.42") + self.assertEqual(record.id, "www:A") self.assertEqual(record.name, "www") self.assertEqual(record.type, RecordType.A) @@ -109,6 +136,15 @@ def test_update_record(self): record = self.driver.update_record( record=record, name="www", type=RecordType.A, data="50.63.202.22" ) + + sent = GoDaddyMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v1/domains/aperture-platform.com/records/A/www") + data = sent.json[0] + self.assertEqual(data["name"], "www") + self.assertEqual(data["type"], "A") + self.assertEqual(data["data"], "50.63.202.22") + self.assertEqual(record.id, "www:A") self.assertEqual(record.name, "www") self.assertEqual(record.type, RecordType.A) @@ -116,6 +152,11 @@ def test_update_record(self): def test_get_zone(self): zone = self.driver.get_zone("aperture-platform.com") + + sent = GoDaddyMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/domains/aperture-platform.com/") + self.assertEqual(zone.id, "177184419") self.assertEqual(zone.domain, "aperture-platform.com") @@ -129,9 +170,14 @@ def test_delete_zone(self): ) self.driver.delete_zone(zone) + sent = GoDaddyMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v1/domains/aperture-platform.com") + class GoDaddyMockHttp(MockHttp): fixtures = DNSFileFixtures("godaddy") + keep_history = True def _v1_domains(self, method, url, body, headers): body = self.fixtures.load("v1_domains.json") From b40c1c2718984536e356e931f1d4c5a3e879c302 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 27 Mar 2025 14:59:00 -0500 Subject: [PATCH 13/52] dns: google: Add API request checking --- libcloud/test/dns/test_google.py | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/libcloud/test/dns/test_google.py b/libcloud/test/dns/test_google.py index 4cd411147b..55393cf7cc 100644 --- a/libcloud/test/dns/test_google.py +++ b/libcloud/test/dns/test_google.py @@ -31,6 +31,7 @@ def setUp(self): GoogleDNSDriver.connectionCls.conn_class = GoogleDNSMockHttp GoogleBaseAuthConnection.conn_class = GoogleAuthMockHttp GoogleDNSMockHttp.type = None + GoogleDNSMockHttp.history.clear() kwargs = DNS_KEYWORD_PARAMS_GOOGLE.copy() kwargs["auth_type"] = "IA" self.driver = GoogleDNSDriver(*DNS_PARAMS_GOOGLE, **kwargs) @@ -40,15 +41,32 @@ def test_default_scopes(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = GoogleDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns/v1/projects/project_name/managedZones") + self.assertEqual(len(zones), 2) def test_list_records(self): zone = self.driver.list_zones()[0] + GoogleDNSMockHttp.history.clear() + records = self.driver.list_records(zone=zone) + + sent = GoogleDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, f"/dns/v1/projects/project_name/managedZones/{zone.id}/rrsets") + self.assertEqual(len(records), 3) def test_get_zone(self): zone = self.driver.get_zone("example-com") + + sent = GoogleDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns/v1/projects/project_name/managedZones/example-com") + self.assertEqual(zone.id, "example-com") self.assertEqual(zone.domain, "example.com.") @@ -65,7 +83,16 @@ def test_get_zone_does_not_exist(self): def test_get_record(self): GoogleDNSMockHttp.type = "FILTER_ZONES" zone = self.driver.list_zones()[0] + GoogleDNSMockHttp.history.clear() + record = self.driver.get_record(zone.id, "A:foo.example.com.") + + # [0] /dns/v1/projects/project_name/managedZones/{zone.id}/rrsets + # [1] /dns/v1/projects/project_name/managedZones/{zone.id} + sent = GoogleDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, f"/dns/v1/projects/project_name/managedZones/{zone.id}/rrsets") + self.assertEqual(record.id, "A:foo.example.com.") self.assertEqual(record.name, "foo.example.com.") self.assertEqual(record.type, "A") @@ -93,6 +120,13 @@ def test_get_record_record_does_not_exist(self): def test_create_zone(self): extra = {"description": "new domain for example.org"} zone = self.driver.create_zone("example.org.", extra) + + sent = GoogleDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/dns/v1/projects/project_name/managedZones") + self.assertEqual(sent.json["name"], "example-org") + self.assertEqual(sent.json["dnsName"], "example.org.") + self.assertEqual(zone.domain, "example.org.") self.assertEqual(zone.extra["description"], extra["description"]) self.assertEqual(len(zone.extra["nameServers"]), 4) @@ -100,6 +134,11 @@ def test_create_zone(self): def test_delete_zone(self): zone = self.driver.get_zone("example-com") res = self.driver.delete_zone(zone) + + sent = GoogleDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/dns/v1/projects/project_name/managedZones/example-com") + self.assertTrue(res) def test_ex_bulk_record_changes(self): @@ -115,6 +154,7 @@ def test_ex_bulk_record_changes(self): class GoogleDNSMockHttp(MockHttp): fixtures = DNSFileFixtures("google") + keep_history = True def _dns_v1_projects_project_name_managedZones(self, method, url, body, headers): if method == "POST": From 728fd5ffb58993e2f4adfb1a7ea2a4418ed48620 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Fri, 28 Mar 2025 10:47:18 -0500 Subject: [PATCH 14/52] dns: liquidweb: Implement test_create_record_success() Leverage the existing get_record.json for this test. --- libcloud/test/dns/test_liquidweb.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/libcloud/test/dns/test_liquidweb.py b/libcloud/test/dns/test_liquidweb.py index a0135f1274..b4d103ebaa 100644 --- a/libcloud/test/dns/test_liquidweb.py +++ b/libcloud/test/dns/test_liquidweb.py @@ -238,7 +238,28 @@ def test_delete_record_RECORD_DOES_NOT_EXIST_ERROR(self): self.fail("Exception was not thrown") def test_create_record_success(self): - pass + LiquidWebMockHttp.type = "CREATE_RECORD_SUCCESS" + zone = Zone( + id="13", + type="master", + ttl=None, + domain="domain.com", + extra={}, + driver=self.driver, + ) + record = self.driver.create_record( + name="nerd.domain.com", + zone=zone, + type=RecordType.A, + data="127.0.0.1", + extra={"ttl": 300}, + ) + + self.assertEqual(record.id, "13") + self.assertEqual(record.type, "A") + self.assertEqual(record.name, "nerd.domain.com") + self.assertEqual(record.data, "127.0.0.1") + self.assertEqual(record.extra.get("ttl"), 300) def test_record_already_exists_error(self): pass @@ -322,7 +343,7 @@ def _v1_Network_DNS_Record_delete_DELETE_RECORD_RECORD_DOES_NOT_EXIST( return (httplib.OK, body, {}, httplib.responses[httplib.OK]) def _v1_Network_DNS_Record_create_CREATE_RECORD_SUCCESS(self, method, url, body, headers): - body = self.fixtures.load("") + body = self.fixtures.load("get_record.json") return (httplib.OK, body, {}, httplib.responses[httplib.OK]) def _v1_Network_DNS_Record_ALREADY_EXISTS_ERROR(self, method, url, body, headers): From dd0c4c4919ec81897cb56b362ae4507a437fc748 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Fri, 28 Mar 2025 10:57:12 -0500 Subject: [PATCH 15/52] dns: liquidweb: Add API request checking --- libcloud/test/dns/test_liquidweb.py | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/libcloud/test/dns/test_liquidweb.py b/libcloud/test/dns/test_liquidweb.py index b4d103ebaa..f188373e89 100644 --- a/libcloud/test/dns/test_liquidweb.py +++ b/libcloud/test/dns/test_liquidweb.py @@ -30,9 +30,18 @@ from libcloud.dns.drivers.liquidweb import LiquidWebDNSDriver +def value_hedge(val): + """ + Return a tuple containing the original value and its string representation, + for use with assertIn() when the value format is inconsistent. + """ + return (val, str(val)) + + class LiquidWebTests(unittest.TestCase): def setUp(self): LiquidWebMockHttp.type = None + LiquidWebMockHttp.history.clear() LiquidWebDNSDriver.connectionCls.conn_class = LiquidWebMockHttp self.driver = LiquidWebDNSDriver(*DNS_PARAMS_LIQUIDWEB) self.test_zone = Zone( @@ -66,6 +75,10 @@ def test_list_zones_empty(self): def test_list_zones_success(self): zones = self.driver.list_zones() + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Zone/list") + self.assertEqual(len(zones), 3) zone = zones[0] @@ -102,6 +115,11 @@ def test_get_zone_success(self): LiquidWebMockHttp.type = "GET_ZONE_SUCCESS" zone = self.driver.get_zone(zone_id="13") + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Zone/details") + self.assertIn(sent.json["params"]["id"], value_hedge(13)) + self.assertEqual(zone.id, "13") self.assertEqual(zone.domain, "blogtest.com") self.assertEqual(zone.type, "NATIVE") @@ -113,6 +131,11 @@ def test_delete_zone_success(self): zone = self.test_zone status = self.driver.delete_zone(zone=zone) + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Zone/delete") + self.assertIn(sent.json["params"]["id"], value_hedge(zone.id)) + self.assertEqual(status, True) def test_delete_zone_zone_does_not_exist(self): @@ -129,6 +152,11 @@ def test_create_zone_success(self): LiquidWebMockHttp.type = "CREATE_ZONE_SUCCESS" zone = self.driver.create_zone(domain="test.com") + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Zone/create") + self.assertEqual(sent.json["params"]["name"], "test.com") + self.assertEqual(zone.id, "13") self.assertEqual(zone.domain, "test.com") self.assertEqual(zone.type, "NATIVE") @@ -156,6 +184,11 @@ def test_list_records_success(self): zone = self.test_zone records = self.driver.list_records(zone=zone) + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Record/list") + self.assertIn(sent.json["params"]["zone_id"], value_hedge(zone.id)) + self.assertEqual(len(records), 3) record = records[0] @@ -193,6 +226,11 @@ def test_get_record_success(self): LiquidWebMockHttp.type = "GET_RECORD_SUCCESS" record = self.driver.get_record(zone_id="13", record_id="13") + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Record/details") + self.assertIn(sent.json["params"]["id"], value_hedge(13)) + self.assertEqual(record.id, "13") self.assertEqual(record.type, "A") self.assertEqual(record.name, "nerd.domain.com") @@ -214,6 +252,15 @@ def test_update_record_success(self): data=record.data, extra={"ttl": 5600}, ) + + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v1/Network/DNS/Record/update") + self.assertIn(sent.json["params"]["id"], value_hedge(13)) + self.assertEqual(sent.json["params"]["name"], "nerd.domain.com") + self.assertEqual(sent.json["params"]["rdata"], "127.0.0.1") + self.assertIn(sent.json["params"]["ttl"], value_hedge(5600)) + self.assertEqual(record1.id, "13") self.assertEqual(record1.type, "A") self.assertEqual(record1.name, "nerd.domain.com") @@ -255,6 +302,14 @@ def test_create_record_success(self): extra={"ttl": 300}, ) + sent = LiquidWebMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/Network/DNS/Record/create") + self.assertIn(sent.json["params"]["zone_id"], value_hedge(13)) + self.assertEqual(sent.json["params"]["name"], "nerd.domain.com") + self.assertEqual(sent.json["params"]["rdata"], "127.0.0.1") + self.assertIn(sent.json["params"]["ttl"], value_hedge(300)) + self.assertEqual(record.id, "13") self.assertEqual(record.type, "A") self.assertEqual(record.name, "nerd.domain.com") @@ -267,6 +322,7 @@ def test_record_already_exists_error(self): class LiquidWebMockHttp(MockHttp): fixtures = DNSFileFixtures("liquidweb") + keep_history = True def _v1_Network_DNS_Zone_list(self, method, url, body, headers): body = self.fixtures.load("zones_list.json") From 7e461aefbea2fbe7ef36f60e05cf5285c59843b9 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Fri, 28 Mar 2025 11:11:36 -0500 Subject: [PATCH 16/52] dns: luadns: Add API request checking --- libcloud/test/dns/test_luadns.py | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/libcloud/test/dns/test_luadns.py b/libcloud/test/dns/test_luadns.py index cc6cb94559..514e1eabed 100644 --- a/libcloud/test/dns/test_luadns.py +++ b/libcloud/test/dns/test_luadns.py @@ -33,6 +33,7 @@ class LuadnsTests(unittest.TestCase): def setUp(self): LuadnsMockHttp.type = None + LuadnsMockHttp.history.clear() LuadnsDNSDriver.connectionCls.conn_class = LuadnsMockHttp self.driver = LuadnsDNSDriver(*DNS_PARAMS_LUADNS) self.test_zone = Zone( @@ -66,6 +67,10 @@ def test_list_zones_empty(self): def test_list_zones_success(self): zones = self.driver.list_zones() + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones") + self.assertEqual(len(zones), 2) zone = zones[0] @@ -95,6 +100,10 @@ def test_get_zone_success(self): LuadnsMockHttp.type = "GET_ZONE_SUCCESS" zone = self.driver.get_zone(zone_id="31") + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones/31") + self.assertEqual(zone.id, "31") self.assertEqual(zone.domain, "example.org") self.assertIsNone(zone.type) @@ -106,6 +115,10 @@ def test_delete_zone_success(self): zone = self.test_zone status = self.driver.delete_zone(zone=zone) + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/v1/zones/{zone.id}") + self.assertEqual(status, True) def test_delete_zone_zone_does_not_exist(self): @@ -122,6 +135,11 @@ def test_create_zone_success(self): LuadnsMockHttp.type = "CREATE_ZONE_SUCCESS" zone = self.driver.create_zone(domain="example.org") + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1/zones") + self.assertEqual(sent.json["name"], "example.org") + self.assertEqual(zone.id, "3") self.assertEqual(zone.domain, "example.org") self.assertIsNone(zone.type) @@ -149,6 +167,10 @@ def test_list_records_success(self): zone = self.test_zone records = self.driver.list_records(zone=zone) + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, f"/v1/zones/{zone.id}/records") + self.assertEqual(len(records), 2) record = records[0] @@ -179,6 +201,10 @@ def test_get_record_success(self): LuadnsMockHttp.type = "GET_RECORD_SUCCESS" record = self.driver.get_record(zone_id="31", record_id="31") + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones/31/records/31") + self.assertEqual(record.id, "31") self.assertEqual(record.type, "MX") self.assertEqual(record.name, "example.com.") @@ -189,6 +215,10 @@ def test_delete_record_success(self): record = self.test_record status = self.driver.delete_record(record=record) + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, f"/v1/zones/{record.zone.id}/records/{record.id}") + self.assertEqual(status, True) def test_delete_record_RECORD_DOES_NOT_EXIST_ERROR(self): @@ -210,6 +240,15 @@ def test_create_record_success(self): data="127.0.0.1", extra={"ttl": 13}, ) + + sent = LuadnsMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, f"/v1/zones/{self.test_zone.id}/records") + self.assertEqual(sent.json["name"], "test.com.") + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["content"], "127.0.0.1") + self.assertEqual(sent.json["ttl"], 13) + self.assertEqual(record.id, "31") self.assertEqual(record.name, "test.com.") self.assertEqual(record.data, "127.0.0.1") @@ -221,6 +260,7 @@ def test_record_already_exists_error(self): class LuadnsMockHttp(MockHttp): fixtures = DNSFileFixtures("luadns") + keep_history = True def _v1_zones(self, method, url, body, headers): body = self.fixtures.load("zones_list.json") From 098a504548daf41d5a2b4d0dd1da0c502033d469 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Fri, 28 Mar 2025 11:30:44 -0500 Subject: [PATCH 17/52] dns: nsone: Fix test_get_record_success() Previously, test_get_record_success() succeeded when receiving a record completely unrelated to the originating .get_record() request. Update the request to align with the expected response. --- libcloud/test/dns/test_nsone.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libcloud/test/dns/test_nsone.py b/libcloud/test/dns/test_nsone.py index 738bc81a79..ae1be8004c 100644 --- a/libcloud/test/dns/test_nsone.py +++ b/libcloud/test/dns/test_nsone.py @@ -134,7 +134,7 @@ def test_get_record_record_does_not_exist(self): def test_get_record_success(self): NsOneMockHttp.type = "GET_RECORD_SUCCESS" - record = self.driver.get_record(zone_id="getrecord.com", record_id="A") + record = self.driver.get_record(zone_id="example.com", record_id="520519509f782d58bb4df419") self.assertEqual(record.id, "520519509f782d58bb4df419") self.assertEqual(record.name, "www.example.com") @@ -301,14 +301,14 @@ def _v1_zones_test_com_example_com_A_DELETE_RECORD_SUCCESS(self, method, url, bo return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_getrecord_com_getrecord_com_A_GET_RECORD_SUCCESS( + def _v1_zones_example_com_example_com_520519509f782d58bb4df419_GET_RECORD_SUCCESS( self, method, url, body, headers ): body = self.fixtures.load("get_record_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_getrecord_com_GET_RECORD_SUCCESS(self, method, url, body, headers): + def _v1_zones_example_com_GET_RECORD_SUCCESS(self, method, url, body, headers): body = self.fixtures.load("get_zone_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] From 7f1190444fe5971bd5f9e9b98c4db9c4df11adf4 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Fri, 28 Mar 2025 11:41:19 -0500 Subject: [PATCH 18/52] dns: nsone: Fix test_list_records_success() Previously, test_list_records_success() succeeded when receiving records completely unrelated to the originating .list_records() request. Update the request to align with the expected response. --- libcloud/test/dns/test_nsone.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libcloud/test/dns/test_nsone.py b/libcloud/test/dns/test_nsone.py index ae1be8004c..ec2f0f8283 100644 --- a/libcloud/test/dns/test_nsone.py +++ b/libcloud/test/dns/test_nsone.py @@ -37,6 +37,14 @@ def setUp(self): NsOneMockHttp.type = None NsOneDNSDriver.connectionCls.conn_class = NsOneMockHttp self.driver = NsOneDNSDriver(*DNS_PARAMS_NSONE) + self.example_zone = Zone( + id="example.com", + type="master", + ttl=None, + domain="example.com", + extra={}, + driver=self, + ) self.test_zone = Zone( id="test.com", type="master", @@ -159,7 +167,7 @@ def test_list_records_empty(self): def test_list_records_success(self): NsOneMockHttp.type = "LIST_RECORDS_SUCCESS" - records = self.driver.list_records(zone=self.test_zone) + records = self.driver.list_records(zone=self.example_zone) self.assertEqual(len(records), 2) arecord = records[1] @@ -274,7 +282,7 @@ def _v1_zones_newzone_com_CREATE_ZONE_ZONE_ALREADY_EXISTS(self, method, url, bod return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_test_com_LIST_RECORDS_SUCCESS(self, method, url, body, headers): + def _v1_zones_example_com_LIST_RECORDS_SUCCESS(self, method, url, body, headers): body = self.fixtures.load("get_zone_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] From f64c3c9cce3705b9edf3461668ec37676c8346ad Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Tue, 1 Apr 2025 16:36:26 -0500 Subject: [PATCH 19/52] dns: nsone: Make NsOneResponse lists instance variables Make NSOneResponse.errors and NSOneResponse.objects instance variables, rather than class variables. This was found due to errors in NSOneDNSDriver unit testing that only appeared when tests were run together, not when tests were run individually. --- libcloud/common/nsone.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libcloud/common/nsone.py b/libcloud/common/nsone.py index f54d2edc7a..07e4e14c58 100644 --- a/libcloud/common/nsone.py +++ b/libcloud/common/nsone.py @@ -24,10 +24,10 @@ class NsOneResponse(JsonResponse): - errors = [] # type: List[Dict] - objects = [] # type: List[Dict] - def __init__(self, response, connection): + self.errors = [] # type: List[Dict] + self.objects = [] # type: List[Dict] + super().__init__(response=response, connection=connection) self.errors, self.objects = self.parse_body_and_errors() if not self.success(): From 96061b4db2fcd54dc0d9656ec8e3495e7e9c889f Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Sat, 29 Mar 2025 10:40:30 -0500 Subject: [PATCH 20/52] dns: Add hostname helpers to Zone class Add .hostname(), .fqdn(), and .prefix() helpers to the Zone class. Each of these functions accepts a host prefix, complete name (unrooted hostname), or FQDN (rooted hostname) associated with the Zone object and returns the format implied by the function name. --- libcloud/dns/base.py | 64 ++++++++++++++++++++++++++++++++++ libcloud/test/dns/test_base.py | 20 +++++++++++ 2 files changed, 84 insertions(+) diff --git a/libcloud/dns/base.py b/libcloud/dns/base.py index f878447449..56bcdd9442 100644 --- a/libcloud/dns/base.py +++ b/libcloud/dns/base.py @@ -102,6 +102,70 @@ def __repr__(self): self.driver.name, ) + def prefix(self, subname): + """ + Accept subordinate (or identity) names in multiple convenience formats. + + In the following examples, "www" is returned: + + | Format | subname | self.domain | + |-----------------|--------------------|---------------| + | Bare host name | "www" | "example.com" | + | FQDN (unrooted) | "www.example.com" | "example.com" | + | FQDN (rooted) | "www.example.com." | "example.com" | + + :param subname: Hostname or FQDN. + :type subname: ``str`` + + :return: Bare record name, without domain part. + :rtype: ``str`` + """ + + return subname.rstrip(".").removesuffix(self.domain).rstrip(".") + + def hostname(self, subname): + """ + Accept subordinate (or identity) names in multiple convenience formats. + + In the following examples, "www.example.com" is returned: + + | Format | subname | self.domain | + |-----------------|--------------------|---------------| + | Bare host name | "www" | "example.com" | + | FQDN (unrooted) | "www.example.com" | "example.com" | + | FQDN (rooted) | "www.example.com." | "example.com" | + + :param subname: Hostname or FQDN. + :type subname: ``str`` + + :return: Complete hostname, including domain part. + :rtype: ``str`` + """ + + prefix = self.prefix(subname) + return f"{prefix}.{self.domain}" if prefix else self.domain + + def fqdn(self, subname): + """ + Accept subordinate (or identity) names in multiple convenience formats. + + In the following examples, "www.example.com." is returned: + + | Format | subname | self.domain | + |-----------------|--------------------|---------------| + | Bare host name | "www" | "example.com" | + | FQDN (unrooted) | "www.example.com" | "example.com" | + | FQDN (rooted) | "www.example.com." | "example.com" | + + :param subname: Hostname or FQDN. + :type subname: ``str`` + + :return: Complete hostname, including domain part. + :rtype: ``str`` + """ + + return f"{self.hostname(subname)}." + class Record: """ diff --git a/libcloud/test/dns/test_base.py b/libcloud/test/dns/test_base.py index 006bb8cd92..338bfed688 100644 --- a/libcloud/test/dns/test_base.py +++ b/libcloud/test/dns/test_base.py @@ -63,6 +63,26 @@ def setUp(self): self.driver = DNSDriver("none", "none") self.tmp_file = tempfile.mkstemp() self.tmp_path = self.tmp_file[1] + self.master_zone = Zone( + id=1, domain="example.com", type="master", ttl=900, driver=self.driver + ) + + def test_zone_helpers(self): + zone = self.master_zone + + for func in (zone.prefix, zone.hostname, zone.fqdn): + with self.assertRaises(AttributeError): + self.assertEqual(func(None)) + + for apex in ("", "example.com", "example.com."): + self.assertEqual(zone.prefix(apex), "") + self.assertEqual(zone.hostname(apex), "example.com") + self.assertEqual(zone.fqdn(apex), "example.com.") + + for sub in ("sub", "sub.example.com", "sub.example.com."): + self.assertEqual(zone.prefix(sub), "sub") + self.assertEqual(zone.hostname(sub), "sub.example.com") + self.assertEqual(zone.fqdn(sub), "sub.example.com.") def test_export_zone_to_bind_format_slave_should_throw(self): zone = Zone(id=1, domain="example.com", type="slave", ttl=900, driver=self.driver) From d0bb79c35ed91b0049f2fc7083307af8b966364f Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 14:57:41 -0500 Subject: [PATCH 21/52] dns: Add {to,from}_default_id() to DNSDriver class Add .to_default_id() and .from_default_id() functions to the DNSDriver class. These functions support the [:] (e.g. "A:www") record ID format used by many of the drivers. --- libcloud/dns/base.py | 25 ++++++++++++++++++++++++- libcloud/test/dns/test_base.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/libcloud/dns/base.py b/libcloud/dns/base.py index 56bcdd9442..385aeff039 100644 --- a/libcloud/dns/base.py +++ b/libcloud/dns/base.py @@ -14,11 +14,13 @@ # limitations under the License. +import re import datetime from typing import Any, Dict, List, Type, Union, Iterator, Optional +from collections import namedtuple from libcloud import __version__ -from libcloud.dns.types import RecordType +from libcloud.dns.types import RecordType, RecordDoesNotExistError from libcloud.common.base import BaseDriver, Connection, ConnectionUserAndKey __all__ = ["Zone", "Record", "DNSDriver"] @@ -277,6 +279,11 @@ class DNSDriver(BaseDriver): # Map libcloud record type enum to provider record type name RECORD_TYPE_MAP = {} # type: Dict[RecordType, str] + # "A" -> type: "A", prefix: None + # "A:www" -> type: "A", prefix: "www" + DEFAULT_ID_RE = re.compile(r"(?P[A-Z]+)(\:(?P.+))?") + DefaultID = namedtuple("DefaultID", "type,name") + def __init__( self, key, # type: str @@ -640,3 +647,19 @@ def _string_to_record_type(self, string): string = string.upper() record_type = getattr(RecordType, string) return record_type + + @staticmethod + def to_default_id(zone, name, type): + prefix = zone.prefix(name) + return f"{type}:{prefix}" if prefix else str(type) + + @classmethod + def from_default_id(cls, zone, record_id): + match = cls.DEFAULT_ID_RE.match(record_id) + if match is None: + raise RecordDoesNotExistError( + value="malformed record ID", driver=cls, record_id=record_id + ) + + name = match.group("name") + return cls.DefaultID(match.group("type"), "" if name is None else zone.prefix(name)) diff --git a/libcloud/test/dns/test_base.py b/libcloud/test/dns/test_base.py index 338bfed688..e6c1aeac95 100644 --- a/libcloud/test/dns/test_base.py +++ b/libcloud/test/dns/test_base.py @@ -182,6 +182,36 @@ def test_get_numeric_id(self): result = record._get_numeric_id() self.assertEqual(result, "") + def test_driver_to_default_id(self): + data = [ + # name, type, id (expected) + ("", "A", "A"), + ("example.com", "A", "A"), + ("example.com.", "A", "A"), + ("mail", "MX", "MX:mail"), + ("mail.example.com", "MX", "MX:mail"), + ("mail.example.com.", "MX", "MX:mail"), + ] + for rname, rtype, rexpect in data: + rid = self.driver.to_default_id(self.master_zone, rname, rtype) + self.assertEqual(rid, rexpect) + + def test_driver_from_default_id(self): + data = [ + # id, name (expected), type (expected) + ("A", "", "A"), # without trailing colon + ("A:", "", "A"), # with trailing colon + ("A:example.com", "", "A"), + ("A:example.com.", "", "A"), + ("MX:mail", "mail", "MX"), + ("MX:mail.example.com", "mail", "MX"), + ("MX:mail.example.com.", "mail", "MX"), + ] + for rid, rname, rtype in data: + rparts = self.driver.from_default_id(self.master_zone, rid) + self.assertEqual(rparts.name, rname) + self.assertEqual(rparts.type, rtype) + def zero_pad(value: int) -> str: if value < 10: From 00bc61599aad06abae2e4839d02e69a34b62501b Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Tue, 1 Apr 2025 17:15:59 -0500 Subject: [PATCH 22/52] dns: nsone: Use locally-relevant zone and record IDs Previously, our NsOneDNSDriver.get_record() function performed lookups using a `record_id` format that differed from the ID embedded within the returned Record instance, so the driver leveraged two independent record ID concepts simultaneously. The NS1 API returns an ID when records are created, but that ID has no local relevance. For example, it cannot be used to request the record from the NS1 API. Make the Record class .id value consistent between create_record() and get_record(z.id, r.id). | zone_id | z.id | r.id | record_id | ---------|----------|----------|---------------|---------------| previous | z.domain | random* | random* | r.type | next | z.domain | z.domain | r.type:r.name | r.type:r.name | * no meaning, effectively random --- libcloud/dns/drivers/nsone.py | 27 ++++++++++++++---------- libcloud/test/dns/test_nsone.py | 37 ++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/libcloud/dns/drivers/nsone.py b/libcloud/dns/drivers/nsone.py index 58c012197d..9b59fc3cc1 100644 --- a/libcloud/dns/drivers/nsone.py +++ b/libcloud/dns/drivers/nsone.py @@ -163,12 +163,15 @@ def get_record(self, zone_id, record_id): :param zone_id: The id of the zone where to search for the record (e.g. example.com) :type zone_id: ``str`` - :param record_id: The type of record to search for - (e.g. A, AAA, MX etc) + :param record_id: The record type joined with the record + name by a colon, if non-empty (e.g. A, AAAA:www, MX:mail, etc) :return: :class:`Record` """ - action = "/v1/zones/{}/{}/{}".format(zone_id, zone_id, record_id) + zone = self.get_zone(zone_id=zone_id) + parts = self.from_default_id(zone, record_id) + rdomain = zone.hostname(parts.name) + action = f"/v1/zones/{zone.domain}/{rdomain}/{parts.type}" try: response = self.connection.request(action=action, method="GET") except NsOneException as e: @@ -176,7 +179,6 @@ def get_record(self, zone_id, record_id): raise RecordDoesNotExistError(value=e.message, driver=self, record_id=record_id) else: raise e - zone = self.get_zone(zone_id=zone_id) record = self._to_record(item=response.parse_body(), zone=zone) return record @@ -213,7 +215,7 @@ def create_record(self, name, zone, type, data, extra=None): :type extra: ``dict`` :return: :class:`Record` """ - record_name = "{}.{}".format(name, zone.domain) if name != "" else zone.domain # noqa + record_name = zone.hostname(name) action = "/v1/zones/{}/{}/{}".format(zone.domain, record_name, type) if type == RecordType.MX: @@ -258,7 +260,7 @@ def update_record(self, record, name, type, data, extra=None): zone = record.zone action = "/v1/zones/{}/{}/{}".format( zone.domain, - "{}.{}".format(name, zone.domain), + zone.hostname(name), type, ) raw_data = {"answers": [{"answer": [data]}]} @@ -300,9 +302,10 @@ def _to_zone(self, item): if key not in common_attr: extra[key] = item.get(key) + zdomain = item["zone"] zone = Zone( - domain=item["zone"], - id=item["id"], + domain=zdomain, + id=zdomain, type=item.get("type"), extra=extra, ttl=extra.get("ttl"), @@ -328,10 +331,12 @@ def _to_record(self, item, zone): data = item.get("answers")[0]["answer"] else: data = item.get("short_answers") + rdomain = item["domain"] + rtype = item["type"] record = Record( - id=item["id"], - name=item["domain"], - type=item["type"], + id=self.to_default_id(zone, rdomain, rtype), + name=rdomain, + type=rtype, data=data, zone=zone, driver=self, diff --git a/libcloud/test/dns/test_nsone.py b/libcloud/test/dns/test_nsone.py index ec2f0f8283..6bf3ad44a8 100644 --- a/libcloud/test/dns/test_nsone.py +++ b/libcloud/test/dns/test_nsone.py @@ -54,9 +54,9 @@ def setUp(self): driver=self, ) self.test_record = Record( - id="13", + id="A", type=RecordType.A, - name="example.com", + name="test.com", zone=self.test_zone, data="127.0.0.1", driver=self, @@ -75,13 +75,13 @@ def test_list_zones_success(self): self.assertEqual(len(zones), 2) zone = zones[0] - self.assertEqual(zone.id, "520422af9f782d37dffb588b") + self.assertEqual(zone.id, "example.com") self.assertIsNone(zone.type) self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.ttl, 3600) zone = zones[1] - self.assertEqual(zone.id, "520422c99f782d37dffb5892") + self.assertEqual(zone.id, "nsoneisgreat.com") self.assertIsNone(zone.type) self.assertEqual(zone.domain, "nsoneisgreat.com") self.assertEqual(zone.ttl, 3600) @@ -115,7 +115,7 @@ def test_create_zone_success(self): NsOneMockHttp.type = "CREATE_ZONE_SUCCESS" zone = self.driver.create_zone(domain="newzone.com") - self.assertEqual(zone.id, "52051b2c9f782d58bb4df41b") + self.assertEqual(zone.id, "newzone.com") self.assertEqual(zone.domain, "newzone.com") self.assertIsNone(zone.type), self.assertEqual(zone.ttl, 3600) @@ -134,7 +134,7 @@ def test_get_record_record_does_not_exist(self): NsOneMockHttp.type = "GET_RECORD_DOES_NOT_EXIST" try: - self.driver.get_record(zone_id="getrecord.com", record_id="A") + self.driver.get_record(zone_id="example.com", record_id="A") except RecordDoesNotExistError as e: self.assertEqual(e.record_id, "A") else: @@ -142,9 +142,9 @@ def test_get_record_record_does_not_exist(self): def test_get_record_success(self): NsOneMockHttp.type = "GET_RECORD_SUCCESS" - record = self.driver.get_record(zone_id="example.com", record_id="520519509f782d58bb4df419") + record = self.driver.get_record(zone_id="example.com", record_id="A:www") - self.assertEqual(record.id, "520519509f782d58bb4df419") + self.assertEqual(record.id, "A:www") self.assertEqual(record.name, "www.example.com") self.assertEqual(record.data, ["1.1.1.1"]) self.assertEqual(record.type, RecordType.A) @@ -171,7 +171,7 @@ def test_list_records_success(self): self.assertEqual(len(records), 2) arecord = records[1] - self.assertEqual(arecord.id, "520519509f782d58bb4df419") + self.assertEqual(arecord.id, "A:www") self.assertEqual(arecord.name, "www.example.com") self.assertEqual(arecord.type, RecordType.A) self.assertEqual(arecord.data, ["1.2.3.4"]) @@ -185,7 +185,7 @@ def test_create_record_success(self): self.test_record.data, self.test_record.extra, ) - self.assertEqual(arecord.id, "608f9619ebe68600ac9f807d") + self.assertEqual(arecord.id, "A") self.assertEqual(arecord.name, "test.com") self.assertEqual(arecord.type, RecordType.A) self.assertEqual(arecord.data, ["127.0.0.1"]) @@ -297,19 +297,19 @@ def _v1_zones_test_com_LIST_RECORDS_ZONE_DOES_NOT_EXIST(self, method, url, body, return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_test_com_example_com_A_DELETE_RECORD_RECORD_DOES_NOT_EXIST( + def _v1_zones_test_com_test_com_A_DELETE_RECORD_RECORD_DOES_NOT_EXIST( self, method, url, body, headers ): body = self.fixtures.load("record_does_not_exist.json") return 404, body, {}, httplib.responses[httplib.OK] - def _v1_zones_test_com_example_com_A_DELETE_RECORD_SUCCESS(self, method, url, body, headers): + def _v1_zones_test_com_test_com_A_DELETE_RECORD_SUCCESS(self, method, url, body, headers): body = self.fixtures.load("delete_record_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_example_com_example_com_520519509f782d58bb4df419_GET_RECORD_SUCCESS( + def _v1_zones_example_com_www_example_com_A_GET_RECORD_SUCCESS( self, method, url, body, headers ): body = self.fixtures.load("get_record_success.json") @@ -321,28 +321,31 @@ def _v1_zones_example_com_GET_RECORD_SUCCESS(self, method, url, body, headers): return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_getrecord_com_getrecord_com_A_GET_RECORD_DOES_NOT_EXIST( + def _v1_zones_example_com_example_com_A_GET_RECORD_DOES_NOT_EXIST( self, method, url, body, headers ): body = self.fixtures.load("record_does_not_exist.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_test_com_example_com_test_com_A_CREATE_RECORD_SUCCESS( + def _v1_zones_example_com_GET_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): + return self._v1_zones_example_com_GET_RECORD_SUCCESS(method, url, body, headers) + + def _v1_zones_test_com_test_com_A_CREATE_RECORD_SUCCESS( self, method, url, body, headers ): body = self.fixtures.load("create_record_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_test_com_example_com_test_com_A_CREATE_RECORD_ALREADY_EXISTS( + def _v1_zones_test_com_test_com_A_CREATE_RECORD_ALREADY_EXISTS( self, method, url, body, headers ): body = self.fixtures.load("create_record_already_exists.json") return 404, body, {}, httplib.responses[httplib.OK] - def _v1_zones_test_com_example_com_test_com_A_CREATE_RECORD_ZONE_NOT_FOUND( + def _v1_zones_test_com_test_com_A_CREATE_RECORD_ZONE_NOT_FOUND( self, method, url, body, headers ): body = self.fixtures.load("create_record_zone_not_found.json") From 72d457195505a82a3d540d4c46f3289cb04fa81f Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Sat, 29 Mar 2025 11:51:06 -0500 Subject: [PATCH 23/52] dns: onapp: Add API request checking * Fix TTL in test_create_record_success() request --- libcloud/test/dns/test_onapp.py | 64 ++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/libcloud/test/dns/test_onapp.py b/libcloud/test/dns/test_onapp.py index d3d952f098..fa66ccb9fa 100644 --- a/libcloud/test/dns/test_onapp.py +++ b/libcloud/test/dns/test_onapp.py @@ -28,6 +28,7 @@ class OnAppDNSTests(LibcloudTestCase): def setUp(self): OnAppDNSDriver.connectionCls.conn_class = OnAppDNSMockHttp OnAppDNSMockHttp.type = None + OnAppDNSMockHttp.history.clear() self.driver = OnAppDNSDriver(*DNS_PARAMS_ONAPP) def assertHasKeys(self, dictionary, keys): @@ -48,6 +49,11 @@ def test_list_record_types(self): def test_list_zones_success(self): zones = self.driver.list_zones() + + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns_zones.json") + self.assertEqual(len(zones), 2) zone1 = zones[0] @@ -66,6 +72,11 @@ def test_list_zones_success(self): def test_get_zone_success(self): zone1 = self.driver.get_zone(zone_id="1") + + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns_zones/1.json") + self.assertEqual(zone1.id, "1") self.assertEqual(zone1.type, "master") self.assertEqual(zone1.domain, "example.com") @@ -82,6 +93,12 @@ def test_get_zone_not_found(self): def test_create_zone_success(self): OnAppDNSMockHttp.type = "CREATE" zone = self.driver.create_zone(domain="example.com") + + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/dns_zones.json") + self.assertEqual(sent.json["dns_zone"]["name"], "example.com") + self.assertEqual(zone.id, "1") self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.ttl, 1200) @@ -93,9 +110,18 @@ def test_delete_zone(self): OnAppDNSMockHttp.type = "DELETE" self.assertTrue(self.driver.delete_zone(zone)) + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/dns_zones/1.json") + def test_list_records_success(self): zone = self.driver.get_zone(zone_id="1") records = self.driver.list_records(zone=zone) + + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns_zones/1/records.json") + self.assertEqual(len(records), 5) record1 = records[0] @@ -120,6 +146,13 @@ def test_list_records_success(self): def test_get_record_success(self): record = self.driver.get_record(zone_id="1", record_id="123") + + # [0] GET /dns_zones/1/records/123.json + # [1] GET /dns_zones/1.json + sent = OnAppDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns_zones/1/records/123.json") + self.assertEqual(record.id, "123") self.assertEqual(record.name, "@") self.assertEqual(record.type, RecordType.A) @@ -129,8 +162,17 @@ def test_create_record_success(self): zone = self.driver.get_zone(zone_id="1") OnAppDNSMockHttp.type = "CREATE" record = self.driver.create_record( - name="blog", zone=zone, type=RecordType.A, data="123.156.189.2" + name="blog", zone=zone, type=RecordType.A, data="123.156.189.2", extra={"ttl": 3600} ) + + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/dns_zones/1/records.json") + self.assertEqual(sent.json["dns_record"]["name"], "blog") + self.assertEqual(sent.json["dns_record"]["type"], "A") + self.assertEqual(sent.json["dns_record"]["ip"], "123.156.189.2") + self.assertEqual(sent.json["dns_record"]["ttl"], 3600) + self.assertEqual(record.id, "111227") self.assertEqual(record.name, "blog") self.assertEqual(record.type, RecordType.A) @@ -139,11 +181,25 @@ def test_create_record_success(self): def test_update_record_success(self): record = self.driver.get_record(zone_id="1", record_id="123") + OnAppDNSMockHttp.history.clear() + OnAppDNSMockHttp.type = "UPDATE" extra = {"ttl": 4500} record1 = self.driver.update_record( record=record, name="@", type=record.type, data="123.156.189.2", extra=extra ) + + # [0] PUT /dns_zones/1/records/123.json + # [1] GET /dns_zones/1/records/123.json + # [2] GET /dns_zones/1.json + sent = OnAppDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/dns_zones/1/records/123.json") + self.assertEqual(sent.json["dns_record"]["name"], "@") + self.assertEqual(sent.json["dns_record"]["type"], "A") + self.assertEqual(sent.json["dns_record"]["ip"], "123.156.189.2") + self.assertEqual(sent.json["dns_record"]["ttl"], 4500) + self.assertEqual(record.data["ip"], "123.156.189.1") self.assertEqual(record.ttl, 3600) self.assertEqual(record1.data["ip"], "123.156.189.2") @@ -153,11 +209,17 @@ def test_delete_record_success(self): record = self.driver.get_record(zone_id="1", record_id="123") OnAppDNSMockHttp.type = "DELETE" status = self.driver.delete_record(record=record) + + sent = OnAppDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/dns_zones/1/records/123.json") + self.assertTrue(status) class OnAppDNSMockHttp(MockHttp): fixtures = DNSFileFixtures("onapp") + keep_history = True def _dns_zones_json(self, method, url, body, headers): body = self.fixtures.load("list_zones.json") From 5915071d94e63438b329bb1f0810d04dc1e99f9b Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Sat, 29 Mar 2025 12:16:33 -0500 Subject: [PATCH 24/52] dns: pointdns: Add API request checking * Align record update with parent zone in test_update_record_success() --- .../pointdns/_zones_1_records_141_UPDATE.json | 2 +- libcloud/test/dns/test_pointdns.py | 68 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/libcloud/test/dns/fixtures/pointdns/_zones_1_records_141_UPDATE.json b/libcloud/test/dns/fixtures/pointdns/_zones_1_records_141_UPDATE.json index f13b76a1c3..2e3f5f8a0b 100644 --- a/libcloud/test/dns/fixtures/pointdns/_zones_1_records_141_UPDATE.json +++ b/libcloud/test/dns/fixtures/pointdns/_zones_1_records_141_UPDATE.json @@ -1,6 +1,6 @@ { "zone_record": { - "name": "updated.com", + "name": "updated.example.com", "data": "1.2.3.5", "id": 141, "aux": null, diff --git a/libcloud/test/dns/test_pointdns.py b/libcloud/test/dns/test_pointdns.py index bb3d551de5..7e94dc1985 100644 --- a/libcloud/test/dns/test_pointdns.py +++ b/libcloud/test/dns/test_pointdns.py @@ -27,6 +27,7 @@ class PointDNSTests(unittest.TestCase): def setUp(self): PointDNSDriver.connectionCls.conn_class = PointDNSMockHttp PointDNSMockHttp.type = None + PointDNSMockHttp.history.clear() self.driver = PointDNSDriver(*DNS_PARAMS_POINTDNS) def assertHasKeys(self, dictionary, keys): @@ -50,6 +51,11 @@ def test_list_record_types(self): def test_list_zones_success(self): PointDNSMockHttp.type = "GET" zones = self.driver.list_zones() + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/zones") + self.assertEqual(len(zones), 2) zone1 = zones[0] @@ -70,6 +76,11 @@ def test_list_records_success(self): PointDNSMockHttp.type = "GET" zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/zones/1/records") + self.assertEqual(len(records), 2) record1 = records[0] @@ -89,6 +100,11 @@ def test_list_records_success(self): def test_get_zone_success(self): PointDNSMockHttp.type = "GET" zone1 = self.driver.get_zone(zone_id="1") + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/zones/1") + self.assertEqual(zone1.id, "1") self.assertEqual(zone1.type, "master") self.assertEqual(zone1.domain, "example.com") @@ -107,6 +123,13 @@ def test_get_zone_zone_not_exists(self): def test_get_record_success(self): PointDNSMockHttp.type = "GET" record = self.driver.get_record(zone_id="1", record_id="141") + + # [0] GET /zones/1/records/141 + # [1] GET /zones/1 + sent = PointDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/zones/1/records/141") + self.assertEqual(record.id, "141") self.assertEqual(record.name, "site.example.com") self.assertEqual(record.type, RecordType.A) @@ -125,6 +148,12 @@ def test_get_record_record_not_exists(self): def test_create_zone_success(self): PointDNSMockHttp.type = "CREATE" zone = self.driver.create_zone(domain="example.com") + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/zones") + self.assertEqual(sent.json["zone"]["name"], "example.com") + self.assertEqual(zone.id, "2") self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.ttl, 3600) @@ -147,6 +176,14 @@ def test_create_record_success(self): record = self.driver.create_record( name="site.example.com", zone=zone, type=RecordType.A, data="1.2.3.4" ) + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/zones/1/records") + self.assertEqual(sent.json["zone_record"]["name"], "site.example.com") + self.assertEqual(sent.json["zone_record"]["record_type"], "A") + self.assertEqual(sent.json["zone_record"]["data"], "1.2.3.4") + self.assertEqual(record.id, "143") self.assertEqual(record.name, "site.example.com") self.assertEqual(record.type, RecordType.A) @@ -172,6 +209,13 @@ def test_update_zone_success(self): PointDNSMockHttp.type = "ZONE_UPDATE" extra = {"user-id": 6} _zone = self.driver.update_zone(zone, zone.domain, zone.ttl, extra=extra) + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/zones/1") + self.assertEqual(sent.json["zone"]["name"], "example.com") + self.assertEqual(sent.json["zone"]["user-id"], 6) + self.assertEqual(_zone.extra.get("user-id"), 6) def test_update_zone_with_error(self): @@ -193,13 +237,24 @@ def test_update_record_success(self): extra = {"ttl": 4500} record1 = self.driver.update_record( record=record, - name="updated.com", + name="updated.example.com", type=RecordType.A, data="1.2.3.5", extra=extra, ) + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/zones/1/records/141") + self.assertEqual(sent.json["zone_record"]["name"], "updated.example.com") + self.assertEqual(sent.json["zone_record"]["record_type"], "A") + self.assertEqual(sent.json["zone_record"]["data"], "1.2.3.5") + self.assertEqual(sent.json["zone_record"]["ttl"], 4500) + + self.assertEqual(record.name, "site.example.com") self.assertEqual(record.data, "1.2.3.4") self.assertEqual(record.extra.get("ttl"), 3600) + self.assertEqual(record1.name, "updated.example.com") self.assertEqual(record1.data, "1.2.3.5") self.assertEqual(record1.extra.get("ttl"), 4500) @@ -226,6 +281,11 @@ def test_delete_zone_success(self): zone = self.driver.list_zones()[0] PointDNSMockHttp.type = "DELETE" status = self.driver.delete_zone(zone=zone) + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/zones/1") + self.assertTrue(status) def test_delete_zone_zone_not_exists(self): @@ -247,6 +307,11 @@ def test_delete_record_success(self): record = records[1] PointDNSMockHttp.type = "DELETE" status = self.driver.delete_record(record=record) + + sent = PointDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/zones/1/records/150") + self.assertTrue(status) def test_delete_record_record_not_exists(self): @@ -556,6 +621,7 @@ def test_ex_delete_mail_redirect_not_found(self): class PointDNSMockHttp(MockHttp): fixtures = DNSFileFixtures("pointdns") + keep_history = True def _zones_GET(self, method, url, body, headers): body = self.fixtures.load("_zones_GET.json") From 72710456306d5079abbddb072e2757a7cd5a7d35 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Sat, 29 Mar 2025 14:00:34 -0500 Subject: [PATCH 25/52] dns: rackspace: Add API request checking * Fix record_id in test_get_record_success() request --- libcloud/test/dns/test_rackspace.py | 82 ++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/libcloud/test/dns/test_rackspace.py b/libcloud/test/dns/test_rackspace.py index 375bc9dd3a..94c8382273 100644 --- a/libcloud/test/dns/test_rackspace.py +++ b/libcloud/test/dns/test_rackspace.py @@ -65,6 +65,7 @@ class RackspaceUSTests(unittest.TestCase): def setUp(self): self.klass.connectionCls.conn_class = RackspaceMockHttp RackspaceMockHttp.type = None + RackspaceMockHttp.history.clear() driver_kwargs = {"region": self.region} self.driver = self.klass(*DNS_PARAMS_RACKSPACE, **driver_kwargs) @@ -108,6 +109,10 @@ def test_list_record_types(self): def test_list_zones_success(self): zones = self.driver.list_zones() + sent = RackspaceMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1.0/11111/domains") + self.assertEqual(len(zones), 6) self.assertEqual(zones[0].domain, "foo4.bar.com") self.assertEqual(zones[0].extra["comment"], "wazaaa") @@ -131,6 +136,10 @@ def test_list_records_success(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) + sent = RackspaceMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063") + self.assertEqual(len(records), 3) self.assertEqual(records[0].name, "test3") self.assertEqual(records[0].type, RecordType.A) @@ -160,6 +169,10 @@ def test_get_zone_success(self): RackspaceMockHttp.type = "GET_ZONE" zone = self.driver.get_zone(zone_id="2946063") + sent = RackspaceMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063") + self.assertEqual(zone.id, "2946063") self.assertEqual(zone.domain, "foo4.bar.com") self.assertEqual(zone.type, "master") @@ -176,7 +189,12 @@ def test_get_zone_does_not_exist(self): self.fail("Exception was not thrown") def test_get_record_success(self): - record = self.driver.get_record(zone_id="12345678", record_id="23456789") + record = self.driver.get_record(zone_id="12345678", record_id="A-7423034") + + sent = RackspaceMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1.0/11111/domains/12345678/records/A-7423034") + self.assertEqual(record.id, "A-7423034") self.assertEqual(record.name, "test3") self.assertEqual(record.type, RecordType.A) @@ -211,6 +229,17 @@ def test_create_zone_success(self): ttl=None, extra={"email": "test@test.com"}, ) + + # [0] POST /v2.0/tokens + # [1] POST /v1.0/{account}/domains + # [2] GET /v1.0/{account}/status/... + sent = RackspaceMockHttp.history.pop(1) + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1.0/11111/domains") + domain = sent.json["domains"][0] + self.assertEqual(domain["name"], "bar.foo1.com") + self.assertEqual(domain["emailAddress"], "test@test.com") + self.assertEqual(zone.id, "2946173") self.assertEqual(zone.domain, "bar.foo1.com") self.assertEqual(zone.type, "master") @@ -238,8 +267,17 @@ def test_create_zone_validaton_error(self): def test_update_zone_success(self): zone = self.driver.list_zones()[0] + sent = RackspaceMockHttp.history.clear() + updated_zone = self.driver.update_zone(zone=zone, extra={"comment": "bar foo"}) + # [0] PUT "/v1.0/{account}/domains/{zone.id}" + # [1] GET "/v1.0/{account}/status/... + sent = RackspaceMockHttp.history.pop(0) + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063") + self.assertIn("comment", sent.json) + self.assertEqual(zone.extra["comment"], "wazaaa") self.assertEqual(updated_zone.id, zone.id) @@ -260,12 +298,23 @@ def test_update_zone_domain_cannot_be_changed(self): def test_create_record_success(self): zone = self.driver.list_zones()[0] + sent = RackspaceMockHttp.history.clear() RackspaceMockHttp.type = "CREATE_RECORD" record = self.driver.create_record( name="www", zone=zone, type=RecordType.A, data="127.1.1.1" ) + # [0] POST /v1.0/{account}/domains/{zone.id}/records + # [1] GET /v1.0/{account}/status/... + sent = RackspaceMockHttp.history.pop(0) + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063/records") + records = sent.json["records"] + self.assertEqual(records[0]["name"], "www.foo4.bar.com") + self.assertEqual(records[0]["type"], "A") + self.assertEqual(records[0]["data"], "127.1.1.1") + self.assertEqual(record.id, "A-7423317") self.assertEqual(record.name, "www") self.assertEqual(record.zone, zone) @@ -276,8 +325,18 @@ def test_create_record_success(self): def test_update_record_success(self): zone = self.driver.list_zones()[0] record = self.driver.list_records(zone=zone)[0] + sent = RackspaceMockHttp.history.clear() + updated_record = self.driver.update_record(record=record, data="127.3.3.3") + # [0] POST /v1.0/{account}/domains/{zone.id}/records/{record.id} + # [1] GET /v1.0/{account}/status/... + sent = RackspaceMockHttp.history.pop(0) + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063/records/A-7423034") + self.assertEqual(sent.json["name"], "test3.foo4.bar.com") + self.assertEqual(sent.json["data"], "127.3.3.3") + self.assertEqual(record.name, "test3") self.assertEqual(record.data, "127.7.7.7") @@ -289,7 +348,16 @@ def test_update_record_success(self): def test_delete_zone_success(self): zone = self.driver.list_zones()[0] + sent = RackspaceMockHttp.history.clear() + status = self.driver.delete_zone(zone=zone) + + # [0] DELETE /v1.0/{account}/domains/{zone.id} + # [1] GET /v1.0/{account}/status/... + sent = RackspaceMockHttp.history.pop(0) + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063") + self.assertTrue(status) def test_delete_zone_does_not_exist(self): @@ -307,7 +375,16 @@ def test_delete_zone_does_not_exist(self): def test_delete_record_success(self): zone = self.driver.list_zones()[0] record = self.driver.list_records(zone=zone)[0] + sent = RackspaceMockHttp.history.clear() + status = self.driver.delete_record(record=record) + + # [0] DELETE /v1.0/{account}/domains/{zone.id}/records/{record.id} + # [1] GET /v1.0/{account}/status/... + sent = RackspaceMockHttp.history.pop(0) + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v1.0/11111/domains/2946063/records/A-7423034") + self.assertTrue(status) def test_delete_record_does_not_exist(self): @@ -416,6 +493,7 @@ class RackspaceUKTests(RackspaceUSTests): class RackspaceMockHttp(MockHttp): fixtures = DNSFileFixtures("rackspace") + keep_history = True base_headers = {"content-type": "application/json"} def _v2_0_tokens(self, method, url, body, headers): @@ -482,7 +560,7 @@ def _v1_0_11111_domains_12345678(self, method, url, body, headers): body = self.fixtures.load("get_zone_success.json") return (httplib.OK, body, self.base_headers, httplib.responses[httplib.OK]) - def _v1_0_11111_domains_12345678_records_23456789(self, method, url, body, headers): + def _v1_0_11111_domains_12345678_records_A_7423034(self, method, url, body, headers): body = self.fixtures.load("get_record_success.json") return (httplib.OK, body, self.base_headers, httplib.responses[httplib.OK]) From 4b9d3645bb1fd1ebdc90768df99fa109c7ffcbdf Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 11:41:59 -0500 Subject: [PATCH 26/52] dns: route53: Fix test_update_record() record type DNS AAAA records are associated with IPv6 addresses. --- libcloud/test/dns/test_route53.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libcloud/test/dns/test_route53.py b/libcloud/test/dns/test_route53.py index 2bc88d8870..48d266c76e 100644 --- a/libcloud/test/dns/test_route53.py +++ b/libcloud/test/dns/test_route53.py @@ -247,7 +247,7 @@ def test_update_record(self): params = { "record": record, "name": "www", - "type": RecordType.A, + "type": RecordType.AAAA, "data": "::1", "extra": {"ttle": 0}, } @@ -255,10 +255,10 @@ def test_update_record(self): self.assertEqual(record.data, "208.111.35.173") - self.assertEqual(updated_record.id, "A:www") + self.assertEqual(updated_record.id, "AAAA:www") self.assertEqual(updated_record.name, "www") self.assertEqual(updated_record.zone, record.zone) - self.assertEqual(updated_record.type, RecordType.A) + self.assertEqual(updated_record.type, RecordType.AAAA) self.assertEqual(updated_record.data, "::1") def test_delete_zone(self): From 47f9ca5c9a6a035052fe184a8847c4f189cd2e8c Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 11:55:17 -0500 Subject: [PATCH 27/52] dns: route53: Add API request checking * Fix "ttle" typo in test_update_record() --- libcloud/test/dns/test_route53.py | 75 ++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/libcloud/test/dns/test_route53.py b/libcloud/test/dns/test_route53.py index 48d266c76e..d7fed5d04b 100644 --- a/libcloud/test/dns/test_route53.py +++ b/libcloud/test/dns/test_route53.py @@ -28,6 +28,7 @@ class Route53Tests(unittest.TestCase): def setUp(self): Route53DNSDriver.connectionCls.conn_class = Route53MockHttp Route53MockHttp.type = None + Route53MockHttp.history.clear() self.driver = Route53DNSDriver(*DNS_PARAMS_ROUTE53) def test_list_record_types(self): @@ -37,6 +38,11 @@ def test_list_record_types(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/2012-02-29/hostedzone") + self.assertEqual(len(zones), 5) zone = zones[0] @@ -47,6 +53,11 @@ def test_list_zones(self): def test_list_records(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) + + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234/rrset") + self.assertEqual(len(records), 10) record = records[1] @@ -81,6 +92,13 @@ def test_get_zone(self): def test_get_record(self): record = self.driver.get_record(zone_id="47234", record_id="CNAME:wibble") + + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234/rrset") + self.assertIn("wibble.t.com", sent.query["name"]) + self.assertIn("CNAME", sent.query["type"]) + self.assertEqual(record.name, "wibble") self.assertEqual(record.type, RecordType.CNAME) self.assertEqual(record.data, "t.com") @@ -130,6 +148,13 @@ def test_get_record_record_does_not_exist(self): def test_create_zone(self): zone = self.driver.create_zone(domain="t.com", type="master", ttl=None, extra=None) + + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/2012-02-29/hostedzone") + xml = sent.body.decode() + self.assertIn("t.com", xml) + self.assertEqual(zone.id, "47234") self.assertEqual(zone.domain, "t.com") @@ -139,6 +164,16 @@ def test_create_record(self): name="www", zone=zone, type=RecordType.A, data="127.0.0.1", extra={"ttl": 0} ) + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234/rrset") + xml = sent.body.decode() + self.assertIn("CREATE", xml) + self.assertIn("www.t.com", xml) + self.assertIn("A", xml) + self.assertIn("127.0.0.1", xml) + self.assertIn("0", xml) + self.assertEqual(record.id, "A:www") self.assertEqual(record.name, "www") self.assertEqual(record.zone, zone) @@ -151,6 +186,16 @@ def test_create_record_zone_name(self): name="", zone=zone, type=RecordType.A, data="127.0.0.1", extra={"ttl": 0} ) + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234/rrset") + xml = sent.body.decode() + self.assertIn("CREATE", xml) + self.assertIn("t.com", xml) + self.assertIn("A", xml) + self.assertIn("127.0.0.1", xml) + self.assertIn("0", xml) + self.assertEqual(record.id, "A:") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) @@ -249,10 +294,23 @@ def test_update_record(self): "name": "www", "type": RecordType.AAAA, "data": "::1", - "extra": {"ttle": 0}, + "extra": {"ttl": 0}, } updated_record = self.driver.update_record(**params) + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234/rrset") + xml = sent.body.decode() + self.assertIn("www.t.com", xml) + self.assertIn("DELETE", xml) + self.assertIn(f"{record.type}", xml) + self.assertIn(f"{record.data}", xml) + self.assertIn("CREATE", xml) + self.assertIn("AAAA", xml) + self.assertIn("::1", xml) + self.assertIn("0", xml) + self.assertEqual(record.data, "208.111.35.173") self.assertEqual(updated_record.id, "AAAA:www") @@ -264,6 +322,11 @@ def test_update_record(self): def test_delete_zone(self): zone = self.driver.list_zones()[0] status = self.driver.delete_zone(zone=zone) + + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234") + self.assertTrue(status) def test_delete_zone_does_not_exist(self): @@ -282,6 +345,15 @@ def test_delete_record(self): zone = self.driver.list_zones()[0] record = self.driver.list_records(zone=zone)[0] status = self.driver.delete_record(record=record) + + sent = Route53MockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/2012-02-29/hostedzone/47234/rrset") + xml = sent.body.decode() + self.assertIn("DELETE", xml) + self.assertIn("wibble.t.com", xml) + self.assertIn("CNAME", xml) + self.assertTrue(status) def test_delete_record_does_not_exist(self): @@ -298,6 +370,7 @@ def test_delete_record_does_not_exist(self): class Route53MockHttp(MockHttp): fixtures = DNSFileFixtures("route53") + keep_history = True def _2012_02_29_hostedzone_47234(self, method, url, body, headers): body = self.fixtures.load("get_zone.xml") From fda0d81bf04658f0cd4dbc52c29fe42eada09ea9 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 12:44:35 -0500 Subject: [PATCH 28/52] dns: worldwidedns: Fix create_zone() reseller suppport The "DYN" parameter is only defined for non-resellers: https://www.worldwidedns.net/dns_api_protocol_new_domain.asp The "ID" parameter is only defined for resellers: https://www.worldwidedns.net/dns_api_protocol_new_domain_reseller.asp --- libcloud/dns/drivers/worldwidedns.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libcloud/dns/drivers/worldwidedns.py b/libcloud/dns/drivers/worldwidedns.py index 80593b499c..6b0f1509b2 100644 --- a/libcloud/dns/drivers/worldwidedns.py +++ b/libcloud/dns/drivers/worldwidedns.py @@ -330,10 +330,12 @@ def create_zone(self, domain, type="master", ttl=None, extra=None): else: dyn = 1 params = {"DOMAIN": domain, "TYPE": _type} - action = "/api_dns_new_domain.asp" - if self.reseller_id is not None: + if self.reseller_id is None: params["DYN"] = dyn + action = "/api_dns_new_domain.asp" + else: + params["ID"] = self.reseller_id action = "/api_dns_new_domain_reseller.asp" self.connection.request(action, params=params) zone = self.get_zone(domain) From a5cd569f60638f32a636b3fa0785fc2d7e70bab8 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 13:52:09 -0500 Subject: [PATCH 29/52] dns: worldwidedns: Add API request checking --- libcloud/test/dns/test_worldwidedns.py | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/libcloud/test/dns/test_worldwidedns.py b/libcloud/test/dns/test_worldwidedns.py index 35655be47f..868f7f2964 100644 --- a/libcloud/test/dns/test_worldwidedns.py +++ b/libcloud/test/dns/test_worldwidedns.py @@ -28,6 +28,7 @@ class WorldWideDNSTests(unittest.TestCase): def setUp(self): WorldWideDNSDriver.connectionCls.conn_class = WorldWideDNSMockHttp WorldWideDNSMockHttp.type = None + WorldWideDNSMockHttp.history.clear() self.driver = WorldWideDNSDriver(*DNS_PARAMS_WORLDWIDEDNS) def assertHasKeys(self, dictionary, keys): @@ -46,6 +47,11 @@ def test_list_record_types(self): def test_list_zones_success(self): zones = self.driver.list_zones() + + sent = WorldWideDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_list_domain.asp") + self.assertEqual(len(zones), 1) zone = zones[0] @@ -76,6 +82,12 @@ def test_list_zones_success(self): def test_list_records_success(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) + + sent = WorldWideDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_list_domain.asp") + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertEqual(len(records), 3) www = records[0] @@ -97,6 +109,12 @@ def test_list_records_zone_does_not_exist(self): def test_get_zone_success(self): zone = self.driver.get_zone(zone_id="niteowebsponsoredthisone.com") + + sent = WorldWideDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_list_domain.asp") + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertEqual(zone.id, "niteowebsponsoredthisone.com") self.assertEqual(zone.type, "master") self.assertEqual(zone.domain, "niteowebsponsoredthisone.com") @@ -156,6 +174,17 @@ def test_get_record_record_does_not_exist(self): def test_create_zone_success(self): zone = self.driver.create_zone(domain="niteowebsponsoredthisone.com", type="master") + + # [0] /api_dns_new_domain.asp + # [1] /api_dns_list.asp + # [2] /api_dns_list_domain.asp + sent = WorldWideDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_new_domain.asp") + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertIn("1", sent.query["DYN"]) + self.assertIn("0", sent.query["TYPE"]) + self.assertEqual(zone.id, "niteowebsponsoredthisone.com") self.assertEqual(zone.domain, "niteowebsponsoredthisone.com") self.assertEqual(zone.ttl, "43200") @@ -173,6 +202,8 @@ def test_create_zone_validaton_error(self): def test_update_zone_success(self): zone = self.driver.list_zones()[0] + WorldWideDNSMockHttp.history.clear() + WorldWideDNSMockHttp.type = "UPDATE_ZONE" updated_zone = self.driver.update_zone( zone=zone, @@ -181,6 +212,14 @@ def test_update_zone_success(self): extra={"HOSTMASTER": "mail.niteowebsponsoredthisone.com"}, ) # noqa + sent = WorldWideDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_modify.asp") + self.assertNotIn("ID", sent.query) + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertIn("3800", sent.query["TTL"]) + self.assertIn("mail.niteowebsponsoredthisone.com", sent.query["HOSTMASTER"]) + self.assertEqual(zone.extra["HOSTMASTER"], "hostmaster.niteowebsponsoredthisone.com") self.assertEqual(updated_zone.id, zone.id) @@ -204,6 +243,8 @@ def test_update_zone_success(self): def test_create_record_success(self): zone = self.driver.list_zones()[0] + WorldWideDNSMockHttp.history.clear() + WorldWideDNSMockHttp.type = "CREATE_RECORD" record = self.driver.create_record( name="domain4", @@ -213,6 +254,18 @@ def test_create_record_success(self): extra={"entry": 4}, ) + # [0] /api_dns_modify.asp + # [1] /api_dns_list_domain.asp + sent = WorldWideDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_modify.asp") + self.assertNotIn("ID", sent.query) + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertIn("domain4", sent.query["S4"]) + self.assertIn("A", sent.query["T4"]) + self.assertIn("0.0.0.4", sent.query["D4"]) + self.assertIn("43200", sent.query["TTL"]) + self.assertEqual(record.id, "4") self.assertEqual(record.name, "domain4") self.assertNotEqual(record.zone.extra.get("S4"), zone.extra.get("S4")) @@ -279,6 +332,8 @@ def test_create_record_max_entry_reached_give_entry(self): def test_update_record_success(self): zone = self.driver.list_zones()[0] record = self.driver.get_record(zone.id, "1") + WorldWideDNSMockHttp.history.clear() + WorldWideDNSMockHttp.type = "UPDATE_RECORD" record = self.driver.update_record( record=record, @@ -288,6 +343,18 @@ def test_update_record_success(self): extra={"entry": 1}, ) + # [0] /api_dns_modify.asp + # [1] /api_dns_list_domain.asp + sent = WorldWideDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_modify.asp") + self.assertNotIn("ID", sent.query) + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertIn("domain1", sent.query["S1"]) + self.assertIn("A", sent.query["T1"]) + self.assertIn("0.0.0.1", sent.query["D1"]) + self.assertIn("43200", sent.query["TTL"]) + self.assertEqual(record.id, "1") self.assertEqual(record.name, "domain1") self.assertNotEqual(record.zone.extra.get("S1"), zone.extra.get("S1")) @@ -298,6 +365,12 @@ def test_update_record_success(self): def test_delete_zone_success(self): zone = self.driver.list_zones()[0] status = self.driver.delete_zone(zone=zone) + + sent = WorldWideDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_delete_domain.asp") + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertTrue(status) def test_delete_zone_does_not_exist(self): @@ -315,10 +388,25 @@ def test_delete_zone_does_not_exist(self): def test_delete_record_success(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) + WorldWideDNSMockHttp.history.clear() + self.assertEqual(len(records), 3) record = records[1] + WorldWideDNSMockHttp.type = "DELETE_RECORD" status = self.driver.delete_record(record=record) + + # [0] /api_dns_modify.asp + # [1] /api_dns_list.asp + # [2] /api_dns_list_domain.asp + sent = WorldWideDNSMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api_dns_modify.asp") + self.assertIn("niteowebsponsoredthisone.com", sent.query["DOMAIN"]) + self.assertIn("www", sent.query["S1"]) + self.assertIn("NONE", sent.query["T2"]) + self.assertIn("@", sent.query["S3"]) + self.assertTrue(status) zone = self.driver.list_zones()[0] records = self.driver.list_records(zone=zone) @@ -327,6 +415,7 @@ def test_delete_record_success(self): class WorldWideDNSMockHttp(MockHttp): fixtures = DNSFileFixtures("worldwidedns") + keep_history = True def _api_dns_list_asp(self, method, url, body, headers): body = self.fixtures.load("api_dns_list") From 7a420e1a12b61abad289e18d34b11d5d0996b39c Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 13:57:08 -0500 Subject: [PATCH 30/52] dns: worldwidedns: Fix update_zone() reseller support Include "ID" parameter when reseller_id is set: https://www.worldwidedns.net/dns_api_protocol_modify_reseller.asp --- libcloud/dns/drivers/worldwidedns.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libcloud/dns/drivers/worldwidedns.py b/libcloud/dns/drivers/worldwidedns.py index 6b0f1509b2..6dded0d4b0 100644 --- a/libcloud/dns/drivers/worldwidedns.py +++ b/libcloud/dns/drivers/worldwidedns.py @@ -226,18 +226,18 @@ def update_zone(self, zone, domain, type="master", ttl=None, extra=None, ex_raw= if extra is not None: params.update(extra) + method = "GET" if ex_raw: action = "/api_dns_modify_raw.asp" if self.reseller_id is not None: action = "/api_dns_modify_raw_reseller.asp" method = "POST" + elif self.reseller_id is not None: + params["ID"] = self.reseller_id + action = "/api_dns_modify_reseller.asp" else: action = "/api_dns_modify.asp" - - if self.reseller_id is not None: - action = "/api_dns_modify_reseller.asp" - method = "GET" response = self.connection.request(action, params=params, method=method) # noqa zone = self.get_zone(zone.id) From 2330fa78ebe73bcd423d1159366c1141d662f2ed Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 14:05:06 -0500 Subject: [PATCH 31/52] dns: worldwidedns: Leverage "slot" ID from Record.id The update_record() and delete_record() functions require a Record instance in their respective argument lists to identify the DNS record to manipulate. Record objects instantiated by this driver contain the assocated "slot" ID within the .id field, so: 1. There is no need for the user to specify the "slot" ID via .update_record(..., extra={"entry": ID}). However, since it was previously required, an "entry" key will be accepted as long as it is consistent with the Record instance. 2. There is no need for .delete_record() to search the Record's zone for the "slot" ID of the Record. --- libcloud/dns/drivers/worldwidedns.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/libcloud/dns/drivers/worldwidedns.py b/libcloud/dns/drivers/worldwidedns.py index 6dded0d4b0..70a4155f45 100644 --- a/libcloud/dns/drivers/worldwidedns.py +++ b/libcloud/dns/drivers/worldwidedns.py @@ -262,15 +262,15 @@ def update_record(self, record, name, type, data, extra=None): :param data: Data for the record (depends on the record type). :type data: ``str`` - :param extra: Contains 'entry' Entry position (1 thru 40) + :param extra: Extra attributes (driver specific). (optional). :type extra: ``dict`` :rtype: :class:`Record` """ - if (extra is None) or ("entry" not in extra): - raise WorldWideDNSError(value="You must enter 'entry' parameter", driver=self) - record_id = extra.get("entry") + record_id = record.id + if "entry" in extra and str(extra["entry"]) != record_id: + raise WorldWideDNSError(value="Inconsistent 'entry' parameter", driver=self) if name == "": name = "@" @@ -437,13 +437,7 @@ def delete_record(self, record): :rtype: ``bool`` """ zone = record.zone - - for index in range(MAX_RECORD_ENTRIES): - if record.name == zone.extra["S%s" % (index + 1)]: - entry = index + 1 - - break - extra = {"S%s" % entry: "", "T%s" % entry: "NONE", "D%s" % entry: ""} + extra = {"S%s" % record.id: "", "T%s" % record.id: "NONE", "D%s" % record.id: ""} self.update_zone(zone, zone.domain, extra=extra) return True From d135f6e14553be876df26879debb2a96798c6c55 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 19:01:08 -0500 Subject: [PATCH 32/52] dns: zonomi: Add API request checking --- libcloud/test/dns/test_zonomi.py | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/libcloud/test/dns/test_zonomi.py b/libcloud/test/dns/test_zonomi.py index e03e691753..dfc9a6065b 100644 --- a/libcloud/test/dns/test_zonomi.py +++ b/libcloud/test/dns/test_zonomi.py @@ -34,6 +34,7 @@ class ZonomiTests(unittest.TestCase): def setUp(self): ZonomiDNSDriver.connectionCls.conn_class = ZonomiMockHttp ZonomiMockHttp.type = None + ZonomiMockHttp.history.clear() self.driver = ZonomiDNSDriver(*DNS_PARAMS_ZONOMI) self.test_zone = Zone( id="zone.com", @@ -69,6 +70,11 @@ def test_list_zones_empty(self): def test_list_zones_success(self): zones = self.driver.list_zones() + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("QUERYZONES", sent.query["action"]) + self.assertEqual(len(zones), 3) zone = zones[0] @@ -105,6 +111,11 @@ def test_get_zone_GET_ZONE_SUCCESS(self): ZonomiMockHttp.type = "GET_ZONE_SUCCESS" zone = self.driver.get_zone(zone_id="gamertest.com") + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("QUERYZONES", sent.query["action"]) + self.assertEqual(zone.id, "gamertest.com") self.assertEqual(zone.domain, "gamertest.com") self.assertEqual(zone.type, "master") @@ -124,6 +135,12 @@ def test_delete_zone_delete_zone_success(self): ZonomiMockHttp.type = "DELETE_ZONE_SUCCESS" status = self.driver.delete_zone(zone=self.test_zone) + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("DELETEZONE", sent.query["action"]) + self.assertIn("zone.com", sent.query["name"]) + self.assertEqual(status, True) def test_create_zone_already_exists(self): @@ -140,6 +157,11 @@ def test_create_zone_create_zone_success(self): zone = self.driver.create_zone(domain="myzone.com") + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/addzone.jsp") + self.assertIn("myzone.com", sent.query["name"]) + self.assertEqual(zone.id, "myzone.com") self.assertEqual(zone.domain, "myzone.com") self.assertEqual(zone.type, "master") @@ -153,6 +175,12 @@ def test_list_records_success(self): ZonomiMockHttp.type = "LIST_RECORDS_SUCCESS" records = self.driver.list_records(zone=self.test_zone) + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("QUERY", sent.query["action"]) + self.assertIn("**.zone.com", sent.query["name"]) + self.assertEqual(len(records), 4) record = records[0] @@ -213,6 +241,12 @@ def test_get_record_success(self): self.driver.get_zone = MagicMock(return_value=zone) record = self.driver.get_record(record_id="oltjano", zone_id="zone.com") + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("QUERY", sent.query["action"]) + self.assertIn("**.zone.com", sent.query["name"]) + self.assertEqual(record.id, "oltjano") self.assertEqual(record.name, "oltjano") self.assertEqual(record.type, "A") @@ -233,6 +267,13 @@ def test_delete_record_success(self): record = self.test_record status = self.driver.delete_record(record=record) + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("DELETE", sent.query["action"]) + self.assertIn("record.zone.com", sent.query["name"]) + self.assertIn("A", sent.query["type"]) + self.assertEqual(status, True) def test_create_record_already_exists(self): @@ -254,6 +295,14 @@ def test_create_record_success(self): name="createrecord", zone=zone, type="A", data="127.0.0.1", extra={} ) + sent = ZonomiMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/app/dns/dyndns.jsp") + self.assertIn("SET", sent.query["action"]) + self.assertIn("createrecord.zone.com", sent.query["name"]) + self.assertIn("A", sent.query["type"]) + self.assertIn("127.0.0.1", sent.query["value"]) + self.assertEqual(record.id, "createrecord") self.assertEqual(record.name, "createrecord") self.assertEqual(record.type, "A") @@ -293,6 +342,7 @@ def test_convert_to_master_couldnt_convert(self): class ZonomiMockHttp(MockHttp): fixtures = DNSFileFixtures("zonomi") + keep_history = True def _app_dns_dyndns_jsp_EMPTY_ZONES_LIST(self, method, url, body, headers): body = self.fixtures.load("empty_zones_list.xml") From 468f10be37e18164b25cdb1b335f774446624405 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Mon, 31 Mar 2025 19:33:56 -0500 Subject: [PATCH 33/52] dns: zonomi: Drop redundant api_key in list_zones() The API key is automatically inserted into the request by the ZonomiConnection class. --- libcloud/dns/drivers/zonomi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libcloud/dns/drivers/zonomi.py b/libcloud/dns/drivers/zonomi.py index ca347367fe..da570466ca 100644 --- a/libcloud/dns/drivers/zonomi.py +++ b/libcloud/dns/drivers/zonomi.py @@ -54,7 +54,7 @@ def list_zones(self): :return: ``list`` of :class:`Zone` """ action = "/app/dns/dyndns.jsp?" - params = {"action": "QUERYZONES", "api_key": self.key} + params = {"action": "QUERYZONES"} response = self.connection.request(action=action, params=params) zones = self._to_zones(response.objects) From d80b61de44aa496a3d7a526a79fc75a666e54b83 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Tue, 1 Apr 2025 17:21:16 -0500 Subject: [PATCH 34/52] dns: nsone: Add test_get_zone_success() The MockHttp function was defined, but the corresponding test was not. --- libcloud/test/dns/test_nsone.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/libcloud/test/dns/test_nsone.py b/libcloud/test/dns/test_nsone.py index 6bf3ad44a8..82c9880326 100644 --- a/libcloud/test/dns/test_nsone.py +++ b/libcloud/test/dns/test_nsone.py @@ -111,6 +111,15 @@ def test_get_zone_zone_does_not_exist(self): else: self.fail("Exception was not thrown") + def test_get_zone_success(self): + NsOneMockHttp.type = "GET_ZONE_SUCCESS" + zone = self.driver.get_zone(zone_id="example.com") + + self.assertEqual(zone.id, "example.com") + self.assertEqual(zone.domain, "example.com") + self.assertIsNone(zone.type), + self.assertEqual(zone.ttl, 3600) + def test_create_zone_success(self): NsOneMockHttp.type = "CREATE_ZONE_SUCCESS" zone = self.driver.create_zone(domain="newzone.com") @@ -250,7 +259,7 @@ def _v1_zones(self, method, url, body, headers): return httplib.OK, body, {}, httplib.responses[httplib.OK] - def _v1_zones_getzone_com_GET_ZONE_SUCCESS(self, method, url, body, headers): + def _v1_zones_example_com_GET_ZONE_SUCCESS(self, method, url, body, headers): body = self.fixtures.load("get_zone_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] @@ -331,9 +340,7 @@ def _v1_zones_example_com_example_com_A_GET_RECORD_DOES_NOT_EXIST( def _v1_zones_example_com_GET_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): return self._v1_zones_example_com_GET_RECORD_SUCCESS(method, url, body, headers) - def _v1_zones_test_com_test_com_A_CREATE_RECORD_SUCCESS( - self, method, url, body, headers - ): + def _v1_zones_test_com_test_com_A_CREATE_RECORD_SUCCESS(self, method, url, body, headers): body = self.fixtures.load("create_record_success.json") return httplib.OK, body, {}, httplib.responses[httplib.OK] From 2d8a8a83baa529119fa91401db7dabf96b255051 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Tue, 1 Apr 2025 17:52:43 -0500 Subject: [PATCH 35/52] dns: nsone: Add API request checking --- libcloud/test/dns/test_nsone.py | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/libcloud/test/dns/test_nsone.py b/libcloud/test/dns/test_nsone.py index 82c9880326..e031c0dc23 100644 --- a/libcloud/test/dns/test_nsone.py +++ b/libcloud/test/dns/test_nsone.py @@ -35,6 +35,7 @@ class NsOneTests(unittest.TestCase): def setUp(self): NsOneMockHttp.type = None + NsOneMockHttp.history.clear() NsOneDNSDriver.connectionCls.conn_class = NsOneMockHttp self.driver = NsOneDNSDriver(*DNS_PARAMS_NSONE) self.example_zone = Zone( @@ -72,6 +73,10 @@ def test_list_zones_empty(self): def test_list_zones_success(self): zones = self.driver.list_zones() + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones") + self.assertEqual(len(zones), 2) zone = zones[0] @@ -100,6 +105,10 @@ def test_delete_zone_success(self): NsOneMockHttp.type = "DELETE_ZONE_SUCCESS" status = self.driver.delete_zone(zone=self.test_zone) + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v1/zones/test.com") + self.assertTrue(status) def test_get_zone_zone_does_not_exist(self): @@ -115,6 +124,10 @@ def test_get_zone_success(self): NsOneMockHttp.type = "GET_ZONE_SUCCESS" zone = self.driver.get_zone(zone_id="example.com") + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones/example.com") + self.assertEqual(zone.id, "example.com") self.assertEqual(zone.domain, "example.com") self.assertIsNone(zone.type), @@ -124,6 +137,11 @@ def test_create_zone_success(self): NsOneMockHttp.type = "CREATE_ZONE_SUCCESS" zone = self.driver.create_zone(domain="newzone.com") + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v1/zones/newzone.com") + self.assertEqual(sent.json["zone"], "newzone.com") + self.assertEqual(zone.id, "newzone.com") self.assertEqual(zone.domain, "newzone.com") self.assertIsNone(zone.type), @@ -153,6 +171,12 @@ def test_get_record_success(self): NsOneMockHttp.type = "GET_RECORD_SUCCESS" record = self.driver.get_record(zone_id="example.com", record_id="A:www") + # [0] /v1/zones/example.com + # [1] /v1/zones/example.com/www.example.com/A + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones/example.com/www.example.com/A") + self.assertEqual(record.id, "A:www") self.assertEqual(record.name, "www.example.com") self.assertEqual(record.data, ["1.1.1.1"]) @@ -177,6 +201,11 @@ def test_list_records_empty(self): def test_list_records_success(self): NsOneMockHttp.type = "LIST_RECORDS_SUCCESS" records = self.driver.list_records(zone=self.example_zone) + + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v1/zones/example.com") + self.assertEqual(len(records), 2) arecord = records[1] @@ -194,6 +223,15 @@ def test_create_record_success(self): self.test_record.data, self.test_record.extra, ) + + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v1/zones/test.com/test.com/A") + self.assertEqual(sent.json["zone"], "test.com") + self.assertEqual(sent.json["domain"], "test.com") + self.assertEqual(sent.json["type"], "A") + self.assertIn({"answer": ["127.0.0.1"]}, sent.json["answers"]) + self.assertEqual(arecord.id, "A") self.assertEqual(arecord.name, "test.com") self.assertEqual(arecord.type, RecordType.A) @@ -243,11 +281,16 @@ def test_delete_record_success(self): NsOneMockHttp.type = "DELETE_RECORD_SUCCESS" status = self.driver.delete_record(record=self.test_record) + sent = NsOneMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v1/zones/test.com/test.com/A") + self.assertTrue(status) class NsOneMockHttp(MockHttp): fixtures = DNSFileFixtures("nsone") + keep_history = True def _v1_zones_EMPTY_ZONES_LIST(self, method, url, body, headers): body = self.fixtures.load("empty_zones_list.json") From df8cb844f20b51a78bb1b60b81b1561be6ae95a4 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 11:07:04 -0500 Subject: [PATCH 36/52] linode: Drop deprecated v3 API support The v3 API became completely unsupported by Linode on July 31, 2023. https://www.linode.com/community/questions/25142/status-of-v3-api-eol --- libcloud/compute/drivers/linode.py | 717 +----------------- libcloud/dns/drivers/linode.py | 260 +------ .../fixtures/linode/_avail_datacenters.json | 36 - .../fixtures/linode/_avail_distributions.json | 246 ------ .../fixtures/linode/_avail_kernels.json | 146 ---- .../fixtures/linode/_avail_linodeplans.json | 158 ---- .../test/compute/fixtures/linode/_batch.json | 22 - .../fixtures/linode/_linode_disk_list.json | 28 - .../fixtures/linode/_linode_ip_list.json | 20 - .../compute/fixtures/linode/_linode_list.json | 33 - libcloud/test/compute/test_linode.py | 208 ----- .../dns/fixtures/linode/create_domain.json | 7 - .../create_domain_validation_error.json | 10 - .../dns/fixtures/linode/create_resource.json | 7 - .../dns/fixtures/linode/delete_domain.json | 7 - .../linode/delete_domain_does_not_exist.json | 10 - .../dns/fixtures/linode/delete_resource.json | 7 - .../delete_resource_does_not_exist.json | 10 - .../test/dns/fixtures/linode/domain_list.json | 36 - .../test/dns/fixtures/linode/get_record.json | 18 - .../linode/get_record_does_not_exist.json | 10 - .../test/dns/fixtures/linode/get_zone.json | 21 - .../linode/get_zone_does_not_exist.json | 10 - .../dns/fixtures/linode/resource_list.json | 30 - .../linode/resource_list_does_not_exist.json | 10 - .../dns/fixtures/linode/update_domain.json | 7 - .../dns/fixtures/linode/update_resource.json | 7 - libcloud/test/dns/test_linode.py | 316 -------- 28 files changed, 3 insertions(+), 2394 deletions(-) delete mode 100644 libcloud/test/compute/fixtures/linode/_avail_datacenters.json delete mode 100644 libcloud/test/compute/fixtures/linode/_avail_distributions.json delete mode 100644 libcloud/test/compute/fixtures/linode/_avail_kernels.json delete mode 100644 libcloud/test/compute/fixtures/linode/_avail_linodeplans.json delete mode 100644 libcloud/test/compute/fixtures/linode/_batch.json delete mode 100644 libcloud/test/compute/fixtures/linode/_linode_disk_list.json delete mode 100644 libcloud/test/compute/fixtures/linode/_linode_ip_list.json delete mode 100644 libcloud/test/compute/fixtures/linode/_linode_list.json delete mode 100644 libcloud/test/compute/test_linode.py delete mode 100644 libcloud/test/dns/fixtures/linode/create_domain.json delete mode 100644 libcloud/test/dns/fixtures/linode/create_domain_validation_error.json delete mode 100644 libcloud/test/dns/fixtures/linode/create_resource.json delete mode 100644 libcloud/test/dns/fixtures/linode/delete_domain.json delete mode 100644 libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json delete mode 100644 libcloud/test/dns/fixtures/linode/delete_resource.json delete mode 100644 libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json delete mode 100644 libcloud/test/dns/fixtures/linode/domain_list.json delete mode 100644 libcloud/test/dns/fixtures/linode/get_record.json delete mode 100644 libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json delete mode 100644 libcloud/test/dns/fixtures/linode/get_zone.json delete mode 100644 libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json delete mode 100644 libcloud/test/dns/fixtures/linode/resource_list.json delete mode 100644 libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json delete mode 100644 libcloud/test/dns/fixtures/linode/update_domain.json delete mode 100644 libcloud/test/dns/fixtures/linode/update_resource.json delete mode 100644 libcloud/test/dns/test_linode.py diff --git a/libcloud/compute/drivers/linode.py b/libcloud/compute/drivers/linode.py index 5d395cbf71..d6a35cd5ae 100644 --- a/libcloud/compute/drivers/linode.py +++ b/libcloud/compute/drivers/linode.py @@ -27,11 +27,8 @@ """ -import os import re import binascii -import itertools -from copy import copy from datetime import datetime from libcloud.utils.py3 import httplib @@ -43,19 +40,12 @@ NodeDriver, NodeLocation, StorageVolume, - NodeAuthSSHKey, - NodeAuthPassword, ) from libcloud.common.linode import ( - API_ROOT, - LINODE_PLAN_IDS, DEFAULT_API_VERSION, - LINODE_DISK_FILESYSTEMS, LINODE_DISK_FILESYSTEMS_V4, LinodeDisk, - LinodeException, LinodeIPAddress, - LinodeConnection, LinodeExceptionV4, LinodeConnectionV4, ) @@ -85,9 +75,7 @@ def __new__( **kwargs, ): if cls is LinodeNodeDriver: - if api_version == "3.0": - cls = LinodeNodeDriverV3 - elif api_version == "4.0": + if api_version == "4.0": cls = LinodeNodeDriverV4 else: raise NotImplementedError( @@ -96,709 +84,6 @@ def __new__( return super().__new__(cls) -class LinodeNodeDriverV3(LinodeNodeDriver): - """libcloud driver for the Linode API - - Rough mapping of which is which: - - - list_nodes linode.list - - reboot_node linode.reboot - - destroy_node linode.delete - - create_node linode.create, linode.update, - linode.disk.createfromdistribution, - linode.disk.create, linode.config.create, - linode.ip.addprivate, linode.boot - - list_sizes avail.linodeplans - - list_images avail.distributions - - list_locations avail.datacenters - - list_volumes linode.disk.list - - destroy_volume linode.disk.delete - - For more information on the Linode API, be sure to read the reference: - - http://www.linode.com/api/ - """ - - connectionCls = LinodeConnection - _linode_plan_ids = LINODE_PLAN_IDS - _linode_disk_filesystems = LINODE_DISK_FILESYSTEMS - features = {"create_node": ["ssh_key", "password"]} - - def __init__( - self, - key, - secret=None, - secure=True, - host=None, - port=None, - api_version=None, - region=None, - **kwargs, - ): - """Instantiate the driver with the given API key - - :param key: the API key to use (required) - :type key: ``str`` - - :rtype: ``None`` - """ - self.datacenter = None - NodeDriver.__init__(self, key) - - # Converts Linode's state from DB to a NodeState constant. - LINODE_STATES = { - (-2): NodeState.UNKNOWN, # Boot Failed - (-1): NodeState.PENDING, # Being Created - 0: NodeState.PENDING, # Brand New - 1: NodeState.RUNNING, # Running - 2: NodeState.STOPPED, # Powered Off - 3: NodeState.REBOOTING, # Shutting Down - 4: NodeState.UNKNOWN, # Reserved - } - - def list_nodes(self): - """ - List all Linodes that the API key can access - - This call will return all Linodes that the API key in use has access - to. - If a node is in this list, rebooting will work; however, creation and - destruction are a separate grant. - - :return: List of node objects that the API key can access - :rtype: ``list`` of :class:`Node` - """ - params = {"api_action": "linode.list"} - data = self.connection.request(API_ROOT, params=params).objects[0] - return self._to_nodes(data) - - def start_node(self, node): - """ - Boot the given Linode - - """ - params = {"api_action": "linode.boot", "LinodeID": node.id} - self.connection.request(API_ROOT, params=params) - return True - - def stop_node(self, node): - """ - Shutdown the given Linode - - """ - params = {"api_action": "linode.shutdown", "LinodeID": node.id} - self.connection.request(API_ROOT, params=params) - return True - - def reboot_node(self, node): - """ - Reboot the given Linode - - Will issue a shutdown job followed by a boot job, using the last booted - configuration. In most cases, this will be the only configuration. - - :param node: the Linode to reboot - :type node: :class:`Node` - - :rtype: ``bool`` - """ - params = {"api_action": "linode.reboot", "LinodeID": node.id} - self.connection.request(API_ROOT, params=params) - return True - - def destroy_node(self, node): - """Destroy the given Linode - - Will remove the Linode from the account and issue a prorated credit. A - grant for removing Linodes from the account is required, otherwise this - method will fail. - - In most cases, all disk images must be removed from a Linode before the - Linode can be removed; however, this call explicitly skips those - safeguards. There is no going back from this method. - - :param node: the Linode to destroy - :type node: :class:`Node` - - :rtype: ``bool`` - """ - params = { - "api_action": "linode.delete", - "LinodeID": node.id, - "skipChecks": True, - } - self.connection.request(API_ROOT, params=params) - return True - - def create_node( - self, - name, - image, - size, - auth, - location=None, - ex_swap=None, - ex_rsize=None, - ex_kernel=None, - ex_payment=None, - ex_comment=None, - ex_private=False, - lconfig=None, - lroot=None, - lswap=None, - ): - """Create a new Linode, deploy a Linux distribution, and boot - - This call abstracts much of the functionality of provisioning a Linode - and getting it booted. A global grant to add Linodes to the account is - required, as this call will result in a billing charge. - - Note that there is a safety valve of 5 Linodes per hour, in order to - prevent a runaway script from ruining your day. - - :keyword name: the name to assign the Linode (mandatory) - :type name: ``str`` - - :keyword image: which distribution to deploy on the Linode (mandatory) - :type image: :class:`NodeImage` - - :keyword size: the plan size to create (mandatory) - :type size: :class:`NodeSize` - - :keyword auth: an SSH key or root password (mandatory) - :type auth: :class:`NodeAuthSSHKey` or :class:`NodeAuthPassword` - - :keyword location: which datacenter to create the Linode in - :type location: :class:`NodeLocation` - - :keyword ex_swap: size of the swap partition in MB (128) - :type ex_swap: ``int`` - - :keyword ex_rsize: size of the root partition in MB (plan size - swap). - :type ex_rsize: ``int`` - - :keyword ex_kernel: a kernel ID from avail.kernels (Latest 2.6 Stable). - :type ex_kernel: ``str`` - - :keyword ex_payment: one of 1, 12, or 24; subscription length (1) - :type ex_payment: ``int`` - - :keyword ex_comment: a small comment for the configuration (libcloud) - :type ex_comment: ``str`` - - :keyword ex_private: whether or not to request a private IP (False) - :type ex_private: ``bool`` - - :keyword lconfig: what to call the configuration (generated) - :type lconfig: ``str`` - - :keyword lroot: what to call the root image (generated) - :type lroot: ``str`` - - :keyword lswap: what to call the swap space (generated) - :type lswap: ``str`` - - :return: Node representing the newly-created Linode - :rtype: :class:`Node` - """ - auth = self._get_and_check_auth(auth) - - # Pick a location (resolves LIBCLOUD-41 in JIRA) - if location: - chosen = location.id - elif self.datacenter: - chosen = self.datacenter - else: - raise LinodeException(0xFB, "Need to select a datacenter first") - - # Step 0: Parameter validation before we purchase - # We're especially careful here so we don't fail after purchase, rather - # than getting halfway through the process and having the API fail. - - # Plan ID - plans = self.list_sizes() - if size.id not in [p.id for p in plans]: - raise LinodeException(0xFB, "Invalid plan ID -- avail.plans") - - # Payment schedule - payment = "1" if not ex_payment else str(ex_payment) - if payment not in ["1", "12", "24"]: - raise LinodeException(0xFB, "Invalid subscription (1, 12, 24)") - - ssh = None - root = None - # SSH key and/or root password - if isinstance(auth, NodeAuthSSHKey): - ssh = auth.pubkey # pylint: disable=no-member - elif isinstance(auth, NodeAuthPassword): - root = auth.password - - if not ssh and not root: - raise LinodeException(0xFB, "Need SSH key or root password") - if root is not None and len(root) < 6: - raise LinodeException(0xFB, "Root password is too short") - - # Swap size - try: - swap = 128 if not ex_swap else int(ex_swap) - except Exception: - raise LinodeException(0xFB, "Need an integer swap size") - - # Root partition size - imagesize = (size.disk - swap) if not ex_rsize else int(ex_rsize) - if (imagesize + swap) > size.disk: - raise LinodeException(0xFB, "Total disk images are too big") - - # Distribution ID - distros = self.list_images() - if image.id not in [d.id for d in distros]: - raise LinodeException(0xFB, "Invalid distro -- avail.distributions") - - # Kernel - if ex_kernel: - kernel = ex_kernel - else: - if image.extra["64bit"]: - # For a list of available kernel ids, see - # https://www.linode.com/kernels/ - kernel = 138 - else: - kernel = 137 - params = {"api_action": "avail.kernels"} - kernels = self.connection.request(API_ROOT, params=params).objects[0] - if kernel not in [z["KERNELID"] for z in kernels]: - raise LinodeException(0xFB, "Invalid kernel -- avail.kernels") - - # Comments - comments = ( - "Created by Apache libcloud " - if not ex_comment - else ex_comment - ) - - # Step 1: linode.create - params = { - "api_action": "linode.create", - "DatacenterID": chosen, - "PlanID": size.id, - "PaymentTerm": payment, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - linode = {"id": data["LinodeID"]} - - # Step 1b. linode.update to rename the Linode - params = { - "api_action": "linode.update", - "LinodeID": linode["id"], - "Label": name, - } - self.connection.request(API_ROOT, params=params) - - # Step 1c. linode.ip.addprivate if it was requested - if ex_private: - params = {"api_action": "linode.ip.addprivate", "LinodeID": linode["id"]} - self.connection.request(API_ROOT, params=params) - - # Step 1d. Labels - # use the linode id as the name can be up to 63 chars and the labels - # are limited to 48 chars - label = { - "lconfig": "[%s] Configuration Profile" % linode["id"], - "lroot": "[{}] {} Disk Image".format(linode["id"], image.name), - "lswap": "[%s] Swap Space" % linode["id"], - } - - if lconfig: - label["lconfig"] = lconfig - - if lroot: - label["lroot"] = lroot - - if lswap: - label["lswap"] = lswap - - # Step 2: linode.disk.createfromdistribution - if not root: - root = binascii.b2a_base64(os.urandom(8)).decode("ascii").strip() - - params = { - "api_action": "linode.disk.createfromdistribution", - "LinodeID": linode["id"], - "DistributionID": image.id, - "Label": label["lroot"], - "Size": imagesize, - "rootPass": root, - } - if ssh: - params["rootSSHKey"] = ssh - data = self.connection.request(API_ROOT, params=params).objects[0] - linode["rootimage"] = data["DiskID"] - - # Step 3: linode.disk.create for swap - params = { - "api_action": "linode.disk.create", - "LinodeID": linode["id"], - "Label": label["lswap"], - "Type": "swap", - "Size": swap, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - linode["swapimage"] = data["DiskID"] - - # Step 4: linode.config.create for main profile - disks = "{},{},,,,,,,".format(linode["rootimage"], linode["swapimage"]) - params = { - "api_action": "linode.config.create", - "LinodeID": linode["id"], - "KernelID": kernel, - "Label": label["lconfig"], - "Comments": comments, - "DiskList": disks, - } - if ex_private: - params["helper_network"] = True - params["helper_distro"] = True - - data = self.connection.request(API_ROOT, params=params).objects[0] - linode["config"] = data["ConfigID"] - - # Step 5: linode.boot - params = { - "api_action": "linode.boot", - "LinodeID": linode["id"], - "ConfigID": linode["config"], - } - self.connection.request(API_ROOT, params=params) - - # Make a node out of it and hand it back - params = {"api_action": "linode.list", "LinodeID": linode["id"]} - data = self.connection.request(API_ROOT, params=params).objects[0] - nodes = self._to_nodes(data) - - if len(nodes) == 1: - node = nodes[0] - if getattr(auth, "generated", False): - node.extra["password"] = auth.password - return node - - return None - - def ex_resize_node(self, node, size): - """Resizes a Linode from one plan to another - - Immediately shuts the Linode down, charges/credits the account, - and issue a migration to another host server. - Requires a size (numeric), which is the desired PlanID available from - avail.LinodePlans() - After resize is complete the node needs to be booted - """ - - params = {"api_action": "linode.resize", "LinodeID": node.id, "PlanID": size} - self.connection.request(API_ROOT, params=params) - return True - - def ex_start_node(self, node): - # NOTE: This method is here for backward compatibility reasons after - # this method was promoted to be part of the standard compute API in - # Libcloud v2.7.0 - return self.start_node(node=node) - - def ex_stop_node(self, node): - # NOTE: This method is here for backward compatibility reasons after - # this method was promoted to be part of the standard compute API in - # Libcloud v2.7.0 - return self.stop_node(node=node) - - def ex_rename_node(self, node, name): - """Renames a node""" - - params = {"api_action": "linode.update", "LinodeID": node.id, "Label": name} - self.connection.request(API_ROOT, params=params) - return True - - def list_sizes(self, location=None): - """ - List available Linode plans - - Gets the sizes that can be used for creating a Linode. Since available - Linode plans vary per-location, this method can also be passed a - location to filter the availability. - - :keyword location: the facility to retrieve plans in - :type location: :class:`NodeLocation` - - :rtype: ``list`` of :class:`NodeSize` - """ - params = {"api_action": "avail.linodeplans"} - data = self.connection.request(API_ROOT, params=params).objects[0] - sizes = [] - for obj in data: - n = NodeSize( - id=obj["PLANID"], - name=obj["LABEL"], - ram=obj["RAM"], - disk=(obj["DISK"] * 1024), - bandwidth=obj["XFER"], - price=obj["PRICE"], - driver=self.connection.driver, - ) - sizes.append(n) - return sizes - - def list_images(self): - """ - List available Linux distributions - - Retrieve all Linux distributions that can be deployed to a Linode. - - :rtype: ``list`` of :class:`NodeImage` - """ - params = {"api_action": "avail.distributions"} - data = self.connection.request(API_ROOT, params=params).objects[0] - distros = [] - for obj in data: - i = NodeImage( - id=obj["DISTRIBUTIONID"], - name=obj["LABEL"], - driver=self.connection.driver, - extra={"pvops": obj["REQUIRESPVOPSKERNEL"], "64bit": obj["IS64BIT"]}, - ) - distros.append(i) - return distros - - def list_locations(self): - """ - List available facilities for deployment - - Retrieve all facilities that a Linode can be deployed in. - - :rtype: ``list`` of :class:`NodeLocation` - """ - params = {"api_action": "avail.datacenters"} - data = self.connection.request(API_ROOT, params=params).objects[0] - nl = [] - for dc in data: - country = None - if "USA" in dc["LOCATION"]: - country = "US" - elif "UK" in dc["LOCATION"]: - country = "GB" - elif "JP" in dc["LOCATION"]: - country = "JP" - else: - country = "??" - nl.append(NodeLocation(dc["DATACENTERID"], dc["LOCATION"], country, self)) - return nl - - def linode_set_datacenter(self, dc): - """ - Set the default datacenter for Linode creation - - Since Linodes must be created in a facility, this function sets the - default that :class:`create_node` will use. If a location keyword is - not passed to :class:`create_node`, this method must have already been - used. - - :keyword dc: the datacenter to create Linodes in unless specified - :type dc: :class:`NodeLocation` - - :rtype: ``bool`` - """ - did = dc.id - params = {"api_action": "avail.datacenters"} - data = self.connection.request(API_ROOT, params=params).objects[0] - for datacenter in data: - if did == dc["DATACENTERID"]: - self.datacenter = did - return - - dcs = ", ".join([d["DATACENTERID"] for d in data]) - self.datacenter = None - raise LinodeException(0xFD, "Invalid datacenter (use one of %s)" % dcs) - - def destroy_volume(self, volume): - """ - Destroys disk volume for the Linode. Linode id is to be provided as - extra["LinodeId"] within :class:`StorageVolume`. It can be retrieved - by :meth:`libcloud.compute.drivers.linode.LinodeNodeDriver\ - .ex_list_volumes`. - - :param volume: Volume to be destroyed - :type volume: :class:`StorageVolume` - - :rtype: ``bool`` - """ - if not isinstance(volume, StorageVolume): - raise LinodeException(0xFD, "Invalid volume instance") - - if volume.extra["LINODEID"] is None: - raise LinodeException(0xFD, "Missing LinodeID") - - params = { - "api_action": "linode.disk.delete", - "LinodeID": volume.extra["LINODEID"], - "DiskID": volume.id, - } - self.connection.request(API_ROOT, params=params) - - return True - - def ex_create_volume(self, size, name, node, fs_type): - """ - Create disk for the Linode. - - :keyword size: Size of volume in megabytes (required) - :type size: ``int`` - - :keyword name: Name of the volume to be created - :type name: ``str`` - - :keyword node: Node to attach volume to. - :type node: :class:`Node` - - :keyword fs_type: The formatted type of this disk. Valid types are: - ext3, ext4, swap, raw - :type fs_type: ``str`` - - - :return: StorageVolume representing the newly-created volume - :rtype: :class:`StorageVolume` - """ - # check node - if not isinstance(node, Node): - raise LinodeException(0xFD, "Invalid node instance") - - # check space available - total_space = node.extra["TOTALHD"] - existing_volumes = self.ex_list_volumes(node) - used_space = 0 - for volume in existing_volumes: - used_space = used_space + volume.size - - available_space = total_space - used_space - if available_space < size: - raise LinodeException( - 0xFD, - "Volume size too big. Available space\ - %d" - % available_space, - ) - - # check filesystem type - if fs_type not in self._linode_disk_filesystems: - raise LinodeException(0xFD, "Not valid filesystem type") - - params = { - "api_action": "linode.disk.create", - "LinodeID": node.id, - "Label": name, - "Type": fs_type, - "Size": size, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - volume = data["DiskID"] - # Make a volume out of it and hand it back - params = { - "api_action": "linode.disk.list", - "LinodeID": node.id, - "DiskID": volume, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - return self._to_volumes(data)[0] - - def ex_list_volumes(self, node, disk_id=None): - """ - List existing disk volumes for for given Linode. - - :keyword node: Node to list disk volumes for. (required) - :type node: :class:`Node` - - :keyword disk_id: Id for specific disk volume. (optional) - :type disk_id: ``int`` - - :rtype: ``list`` of :class:`StorageVolume` - """ - if not isinstance(node, Node): - raise LinodeException(0xFD, "Invalid node instance") - - params = {"api_action": "linode.disk.list", "LinodeID": node.id} - # Add param if disk_id was specified - if disk_id is not None: - params["DiskID"] = disk_id - - data = self.connection.request(API_ROOT, params=params).objects[0] - return self._to_volumes(data) - - def _to_volumes(self, objs): - """ - Convert returned JSON volumes into StorageVolume instances - - :keyword objs: ``list`` of JSON dictionaries representing the - StorageVolumes - :type objs: ``list`` - - :return: ``list`` of :class:`StorageVolume`s - """ - volumes = {} - for o in objs: - vid = o["DISKID"] - volumes[vid] = vol = StorageVolume( - id=vid, - name=o["LABEL"], - size=int(o["SIZE"]), - driver=self.connection.driver, - ) - vol.extra = copy(o) - return list(volumes.values()) - - def _to_nodes(self, objs): - """Convert returned JSON Linodes into Node instances - - :keyword objs: ``list`` of JSON dictionaries representing the Linodes - :type objs: ``list`` - :return: ``list`` of :class:`Node`s""" - - # Get the IP addresses for the Linodes - nodes = {} - batch = [] - for o in objs: - lid = o["LINODEID"] - nodes[lid] = n = Node( - id=lid, - name=o["LABEL"], - public_ips=[], - private_ips=[], - state=self.LINODE_STATES[o["STATUS"]], - driver=self.connection.driver, - ) - n.extra = copy(o) - n.extra["PLANID"] = self._linode_plan_ids.get(o.get("TOTALRAM")) - batch.append({"api_action": "linode.ip.list", "LinodeID": lid}) - - # Avoid batch limitation - ip_answers = [] - args = [iter(batch)] * 25 - - for twenty_five in itertools.zip_longest(*args): - twenty_five = [q for q in twenty_five if q] - params = { - "api_action": "batch", - "api_requestArray": json.dumps(twenty_five), - } - req = self.connection.request(API_ROOT, params=params) - if not req.success() or len(req.objects) == 0: - return None - ip_answers.extend(req.objects) - - # Add the returned IPs to the nodes and return them - for ip_list in ip_answers: - for ip in ip_list: - lid = ip["LINODEID"] - which = nodes[lid].public_ips if ip["ISPUBLIC"] == 1 else nodes[lid].private_ips - which.append(ip["IPADDRESS"]) - return list(nodes.values()) - - class LinodeNodeDriverV4(LinodeNodeDriver): connectionCls = LinodeConnectionV4 _linode_disk_filesystems = LINODE_DISK_FILESYSTEMS_V4 diff --git a/libcloud/dns/drivers/linode.py b/libcloud/dns/drivers/linode.py index b2f609f254..b93e743b88 100644 --- a/libcloud/dns/drivers/linode.py +++ b/libcloud/dns/drivers/linode.py @@ -20,13 +20,11 @@ from libcloud.dns.base import Zone, Record, DNSDriver from libcloud.dns.types import Provider, RecordType, ZoneDoesNotExistError, RecordDoesNotExistError from libcloud.utils.py3 import httplib -from libcloud.utils.misc import get_new_obj, merge_valid_keys +from libcloud.utils.misc import merge_valid_keys from libcloud.common.linode import ( - API_ROOT, DEFAULT_API_VERSION, LinodeResponse, LinodeException, - LinodeConnection, LinodeResponseV4, LinodeExceptionV4, LinodeConnectionV4, @@ -88,9 +86,7 @@ def __new__( **kwargs, ): if cls is LinodeDNSDriver: - if api_version == "3.0": - cls = LinodeDNSDriverV3 - elif api_version == "4.0": + if api_version == "4.0": cls = LinodeDNSDriverV4 else: raise NotImplementedError( @@ -117,258 +113,6 @@ def _make_excp(self, error): return result -class LinodeDNSConnection(LinodeConnection): - responseCls = LinodeDNSResponse - - -class LinodeDNSDriverV3(LinodeDNSDriver): - connectionCls = LinodeDNSConnection - - RECORD_TYPE_MAP = { - RecordType.NS: "NS", - RecordType.MX: "MX", - RecordType.A: "A", - RecordType.AAAA: "AAAA", - RecordType.CNAME: "CNAME", - RecordType.TXT: "TXT", - RecordType.SRV: "SRV", - } - - def list_zones(self): - params = {"api_action": "domain.list"} - data = self.connection.request(API_ROOT, params=params).objects[0] - zones = self._to_zones(data) - return zones - - def list_records(self, zone): - params = {"api_action": "domain.resource.list", "DOMAINID": zone.id} - - self.connection.set_context(context={"resource": "zone", "id": zone.id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - records = self._to_records(items=data, zone=zone) - return records - - def get_zone(self, zone_id): - params = {"api_action": "domain.list", "DomainID": zone_id} - self.connection.set_context(context={"resource": "zone", "id": zone_id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - zones = self._to_zones(data) - - if len(zones) != 1: - raise ZoneDoesNotExistError(value="", driver=self, zone_id=zone_id) - - return zones[0] - - def get_record(self, zone_id, record_id): - zone = self.get_zone(zone_id=zone_id) - params = { - "api_action": "domain.resource.list", - "DomainID": zone_id, - "ResourceID": record_id, - } - self.connection.set_context(context={"resource": "record", "id": record_id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - records = self._to_records(items=data, zone=zone) - - if len(records) != 1: - raise RecordDoesNotExistError(value="", driver=self, record_id=record_id) - - return records[0] - - def create_zone(self, domain, type="master", ttl=None, extra=None): - """ - Create a new zone. - - API docs: http://www.linode.com/api/dns/domain.create - """ - params = {"api_action": "domain.create", "Type": type, "Domain": domain} - - if ttl: - params["TTL_sec"] = ttl - - merged = merge_valid_keys(params=params, valid_keys=VALID_ZONE_EXTRA_PARAMS, extra=extra) - data = self.connection.request(API_ROOT, params=params).objects[0] - zone = Zone( - id=data["DomainID"], - domain=domain, - type=type, - ttl=ttl, - extra=merged, - driver=self, - ) - return zone - - def update_zone(self, zone, domain=None, type=None, ttl=None, extra=None): - """ - Update an existing zone. - - API docs: http://www.linode.com/api/dns/domain.update - """ - params = {"api_action": "domain.update", "DomainID": zone.id} - - if type: - params["Type"] = type - - if domain: - params["Domain"] = domain - - if ttl: - params["TTL_sec"] = ttl - - merged = merge_valid_keys(params=params, valid_keys=VALID_ZONE_EXTRA_PARAMS, extra=extra) - self.connection.request(API_ROOT, params=params).objects[0] - updated_zone = get_new_obj( - obj=zone, - klass=Zone, - attributes={"domain": domain, "type": type, "ttl": ttl, "extra": merged}, - ) - return updated_zone - - def create_record(self, name, zone, type, data, extra=None): - """ - Create a new record. - - API docs: http://www.linode.com/api/dns/domain.resource.create - """ - params = { - "api_action": "domain.resource.create", - "DomainID": zone.id, - "Name": name, - "Target": data, - "Type": self.RECORD_TYPE_MAP[type], - } - merged = merge_valid_keys(params=params, valid_keys=VALID_RECORD_EXTRA_PARAMS, extra=extra) - - result = self.connection.request(API_ROOT, params=params).objects[0] - record = Record( - id=result["ResourceID"], - name=name, - type=type, - data=data, - extra=merged, - zone=zone, - driver=self, - ttl=merged.get("TTL_sec", None), - ) - return record - - def update_record(self, record, name=None, type=None, data=None, extra=None): - """ - Update an existing record. - - API docs: http://www.linode.com/api/dns/domain.resource.update - """ - params = { - "api_action": "domain.resource.update", - "ResourceID": record.id, - "DomainID": record.zone.id, - } - - if name: - params["Name"] = name - - if data: - params["Target"] = data - - if type is not None: - params["Type"] = self.RECORD_TYPE_MAP[type] - - merged = merge_valid_keys(params=params, valid_keys=VALID_RECORD_EXTRA_PARAMS, extra=extra) - - self.connection.request(API_ROOT, params=params).objects[0] - updated_record = get_new_obj( - obj=record, - klass=Record, - attributes={"name": name, "data": data, "type": type, "extra": merged}, - ) - return updated_record - - def delete_zone(self, zone): - params = {"api_action": "domain.delete", "DomainID": zone.id} - - self.connection.set_context(context={"resource": "zone", "id": zone.id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - - return "DomainID" in data - - def delete_record(self, record): - params = { - "api_action": "domain.resource.delete", - "DomainID": record.zone.id, - "ResourceID": record.id, - } - - self.connection.set_context(context={"resource": "record", "id": record.id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - - return "ResourceID" in data - - def _to_zones(self, items): - """ - Convert a list of items to the Zone objects. - """ - zones = [] - - for item in items: - zones.append(self._to_zone(item)) - - return zones - - def _to_zone(self, item): - """ - Build an Zone object from the item dictionary. - """ - extra = { - "SOA_Email": item["SOA_EMAIL"], - "status": item["STATUS"], - "description": item["DESCRIPTION"], - } - zone = Zone( - id=item["DOMAINID"], - domain=item["DOMAIN"], - type=item["TYPE"], - ttl=item["TTL_SEC"], - driver=self, - extra=extra, - ) - return zone - - def _to_records(self, items, zone=None): - """ - Convert a list of items to the Record objects. - """ - records = [] - - for item in items: - records.append(self._to_record(item=item, zone=zone)) - - return records - - def _to_record(self, item, zone=None): - """ - Build a Record object from the item dictionary. - """ - extra = { - "protocol": item["PROTOCOL"], - "ttl_sec": item["TTL_SEC"], - "port": item["PORT"], - "weight": item["WEIGHT"], - "priority": item["PRIORITY"], - } - type = self._string_to_record_type(item["TYPE"]) - record = Record( - id=item["RESOURCEID"], - name=item["NAME"], - type=type, - data=item["TARGET"], - zone=zone, - driver=self, - ttl=item["TTL_SEC"], - extra=extra, - ) - return record - - class LinodeDNSResponseV4(LinodeResponseV4): pass diff --git a/libcloud/test/compute/fixtures/linode/_avail_datacenters.json b/libcloud/test/compute/fixtures/linode/_avail_datacenters.json deleted file mode 100644 index eb169623b1..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_datacenters.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "LOCATION": "Dallas, TX, USA", - "DATACENTERID": 2, - "ABBR": "dallas" - }, - { - "LOCATION": "Fremont, CA, USA", - "DATACENTERID": 3, - "ABBR": "fremont" - }, - { - "LOCATION": "Atlanta, GA, USA", - "DATACENTERID": 4, - "ABBR": "atlanta" - }, - { - "LOCATION": "Newark, NJ, USA", - "DATACENTERID": 6, - "ABBR": "newark" - }, - { - "LOCATION": "London, England, UK", - "DATACENTERID": 7, - "ABBR": "london" - }, - { - "LOCATION": "Tokyo, JP", - "DATACENTERID": 8, - "ABBR": "tokyo" - } - ], - "ACTION": "avail.datacenters" -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/linode/_avail_distributions.json b/libcloud/test/compute/fixtures/linode/_avail_distributions.json deleted file mode 100644 index f1bdee61b9..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_distributions.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 112, - "IS64BIT": 1, - "LABEL": "Arch Linux 2013.06", - "MINIMAGESIZE": 500, - "CREATE_DT": "2013-06-06 02:45:11.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 89, - "IS64BIT": 1, - "LABEL": "CentOS 6.2", - "MINIMAGESIZE": 800, - "CREATE_DT": "2011-07-19 11:38:20.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 78, - "IS64BIT": 1, - "LABEL": "Debian 6", - "MINIMAGESIZE": 550, - "CREATE_DT": "2011-02-08 16:54:31.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 109, - "IS64BIT": 1, - "LABEL": "Debian 7", - "MINIMAGESIZE": 660, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 114, - "IS64BIT": 1, - "LABEL": "Fedora 19", - "MINIMAGESIZE": 750, - "CREATE_DT": "2013-08-26 15:29:21.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 53, - "IS64BIT": 1, - "LABEL": "Gentoo", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2009-04-04 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 115, - "IS64BIT": 1, - "LABEL": "openSUSE 12.3", - "MINIMAGESIZE": 1024, - "CREATE_DT": "2013-09-19 10:49:09.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 87, - "IS64BIT": 1, - "LABEL": "Slackware 13.37", - "MINIMAGESIZE": 600, - "CREATE_DT": "2011-06-05 15:11:59.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 65, - "IS64BIT": 1, - "LABEL": "Ubuntu 10.04 LTS", - "MINIMAGESIZE": 450, - "CREATE_DT": "2010-04-29 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 99, - "IS64BIT": 1, - "LABEL": "Ubuntu 12.04 LTS", - "MINIMAGESIZE": 600, - "CREATE_DT": "2012-04-26 17:25:16.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 111, - "IS64BIT": 1, - "LABEL": "Ubuntu 13.04", - "MINIMAGESIZE": 770, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 113, - "IS64BIT": 0, - "LABEL": "Arch Linux 2013.06 32bit", - "MINIMAGESIZE": 500, - "CREATE_DT": "2013-06-06 02:45:11.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 88, - "IS64BIT": 0, - "LABEL": "CentOS 6.2 32bit", - "MINIMAGESIZE": 800, - "CREATE_DT": "2011-07-19 11:38:20.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 77, - "IS64BIT": 0, - "LABEL": "Debian 6 32bit", - "MINIMAGESIZE": 550, - "CREATE_DT": "2011-02-08 16:54:31.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 108, - "IS64BIT": 0, - "LABEL": "Debian 7 32bit", - "MINIMAGESIZE": 660, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 72, - "IS64BIT": 0, - "LABEL": "Gentoo 32bit", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2010-09-13 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 86, - "IS64BIT": 0, - "LABEL": "Slackware 13.37 32bit", - "MINIMAGESIZE": 600, - "CREATE_DT": "2011-06-05 15:11:59.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 64, - "IS64BIT": 0, - "LABEL": "Ubuntu 10.04 LTS 32bit", - "MINIMAGESIZE": 450, - "CREATE_DT": "2010-04-29 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 98, - "IS64BIT": 0, - "LABEL": "Ubuntu 12.04 LTS 32bit", - "MINIMAGESIZE": 600, - "CREATE_DT": "2012-04-26 17:25:16.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 110, - "IS64BIT": 0, - "LABEL": "Ubuntu 13.04 32bit", - "MINIMAGESIZE": 770, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 105, - "IS64BIT": 1, - "LABEL": "Arch Linux 2012.10", - "MINIMAGESIZE": 500, - "CREATE_DT": "2012-10-22 15:00:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 60, - "IS64BIT": 1, - "LABEL": "CentOS 5.6 64bit", - "MINIMAGESIZE": 950, - "CREATE_DT": "2009-08-17 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 100, - "IS64BIT": 1, - "LABEL": "Fedora 17", - "MINIMAGESIZE": 800, - "CREATE_DT": "2012-05-31 16:03:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 97, - "IS64BIT": 1, - "LABEL": "openSUSE 12.1", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2012-04-13 11:43:30.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 107, - "IS64BIT": 1, - "LABEL": "Ubuntu 12.10", - "MINIMAGESIZE": 660, - "CREATE_DT": "2012-11-06 11:51:25.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 104, - "IS64BIT": 0, - "LABEL": "Arch Linux 2012.10 32bit", - "MINIMAGESIZE": 500, - "CREATE_DT": "2012-10-22 15:00:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 59, - "IS64BIT": 0, - "LABEL": "CentOS 5.6 32bit", - "MINIMAGESIZE": 950, - "CREATE_DT": "2009-08-17 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 101, - "IS64BIT": 0, - "LABEL": "Fedora 17 32bit", - "MINIMAGESIZE": 800, - "CREATE_DT": "2012-05-31 16:03:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 96, - "IS64BIT": 0, - "LABEL": "openSUSE 12.1 32bit", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2012-04-13 11:43:30.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 106, - "IS64BIT": 0, - "LABEL": "Ubuntu 12.10 32bit", - "MINIMAGESIZE": 660, - "CREATE_DT": "2012-11-06 11:51:25.0" - } - ], - "ACTION": "avail.distributions" -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/linode/_avail_kernels.json b/libcloud/test/compute/fixtures/linode/_avail_kernels.json deleted file mode 100644 index 9552c46c5f..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_kernels.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "avail.kernels", - "DATA": [ - { - "LABEL": "Latest 2.6 Stable (2.6.18.8-linode19)", - "ISXEN": 1, - "KERNELID": 60 - }, - { - "LABEL": "2.6.18.8-linode19", - "ISXEN": 1, - "KERNELID": 103 - }, - { - "LABEL": "2.6.30.5-linode20", - "ISXEN": 1, - "KERNELID": 105 - }, - { - "LABEL": "Latest 2.6 Stable (2.6.18.8-x86_64-linode7)", - "ISXEN": 1, - "KERNELID": 107 - }, - { - "LABEL": "2.6.18.8-x86_64-linode7", - "ISXEN": 1, - "KERNELID": 104 - }, - { - "LABEL": "2.6.30.5-x86_64-linode8", - "ISXEN": 1, - "KERNELID": 106 - }, - { - "LABEL": "pv-grub-x86_32", - "ISXEN": 1, - "KERNELID": 92 - }, - { - "LABEL": "pv-grub-x86_64", - "ISXEN": 1, - "KERNELID": 95 - }, - { - "LABEL": "Recovery - Finnix (kernel)", - "ISXEN": 1, - "KERNELID": 61 - }, - { - "LABEL": "2.6.18.8-domU-linode7", - "ISXEN": 1, - "KERNELID": 81 - }, - { - "LABEL": "2.6.18.8-linode10", - "ISXEN": 1, - "KERNELID": 89 - }, - { - "LABEL": "2.6.18.8-linode16", - "ISXEN": 1, - "KERNELID": 98 - }, - { - "LABEL": "2.6.24.4-linode8", - "ISXEN": 1, - "KERNELID": 84 - }, - { - "LABEL": "2.6.25-linode9", - "ISXEN": 1, - "KERNELID": 88 - }, - { - "LABEL": "2.6.25.10-linode12", - "ISXEN": 1, - "KERNELID": 90 - }, - { - "LABEL": "2.6.26-linode13", - "ISXEN": 1, - "KERNELID": 91 - }, - { - "LABEL": "2.6.27.4-linode14", - "ISXEN": 1, - "KERNELID": 93 - }, - { - "LABEL": "2.6.28-linode15", - "ISXEN": 1, - "KERNELID": 96 - }, - { - "LABEL": "2.6.28.3-linode17", - "ISXEN": 1, - "KERNELID": 99 - }, - { - "LABEL": "2.6.29-linode18", - "ISXEN": 1, - "KERNELID": 101 - }, - { - "LABEL": "2.6.16.38-x86_64-linode2", - "ISXEN": 1, - "KERNELID": 85 - }, - { - "LABEL": "2.6.18.8-x86_64-linode1", - "ISXEN": 1, - "KERNELID": 86 - }, - { - "LABEL": "2.6.27.4-x86_64-linode3", - "ISXEN": 1, - "KERNELID": 94 - }, - { - "LABEL": "2.6.28-x86_64-linode4", - "ISXEN": 1, - "KERNELID": 97 - }, - { - "LABEL": "2.6.28.3-x86_64-linode5", - "ISXEN": 1, - "KERNELID": 100 - }, - { - "LABEL": "2.6.29-x86_64-linode6", - "ISXEN": 1, - "KERNELID": 102 - }, - { - "LABEL": "3.9.3-x86-linode52", - "ISXEN": 1, - "KERNELID": 137 - }, - { - "LABEL": "3.9.3-x86_64-linode33", - "ISXEN": 1, - "KERNELID": 138 - } - ] -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/linode/_avail_linodeplans.json b/libcloud/test/compute/fixtures/linode/_avail_linodeplans.json deleted file mode 100644 index ac2488934f..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_linodeplans.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [{ - "CORES": 1, - "PRICE": 10.00, - "RAM": 1024, - "XFER": 2000, - "PLANID": 1, - "LABEL": "Linode 1024", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 24, - "HOURLY": 0.0150 - }, { - "CORES": 2, - "PRICE": 20.00, - "RAM": 2048, - "XFER": 3000, - "PLANID": 2, - "LABEL": "Linode 2048", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 48, - "HOURLY": 0.0300 - }, { - "CORES": 4, - "PRICE": 40.00, - "RAM": 4096, - "XFER": 4000, - "PLANID": 4, - "LABEL": "Linode 4096", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 96, - "HOURLY": 0.0600 - }, { - "CORES": 6, - "PRICE": 80.00, - "RAM": 8192, - "XFER": 8000, - "PLANID": 6, - "LABEL": "Linode 8192", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 192, - "HOURLY": 0.1200 - }, { - "CORES": 8, - "PRICE": 160.00, - "RAM": 16384, - "XFER": 16000, - "PLANID": 7, - "LABEL": "Linode 16384", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 384, - "HOURLY": 0.2400 - }, { - "CORES": 12, - "PRICE": 320.00, - "RAM": 32768, - "XFER": 20000, - "PLANID": 8, - "LABEL": "Linode 32768", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 768, - "HOURLY": 0.4800 - }, { - "CORES": 16, - "PRICE": 480.00, - "RAM": 49152, - "XFER": 20000, - "PLANID": 9, - "LABEL": "Linode 49152", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 1152, - "HOURLY": 0.7200 - }, { - "CORES": 20, - "PRICE": 640.00, - "RAM": 65536, - "XFER": 20000, - "PLANID": 10, - "LABEL": "Linode 65536", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 1536, - "HOURLY": 0.9600 - }, { - "CORES": 20, - "PRICE": 960.00, - "RAM": 98304, - "XFER": 20000, - "PLANID": 12, - "LABEL": "Linode 98304", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 1920, - "HOURLY": 1.4400 - }], - "ACTION": "avail.linodeplans" -} diff --git a/libcloud/test/compute/fixtures/linode/_batch.json b/libcloud/test/compute/fixtures/linode/_batch.json deleted file mode 100644 index 36e28f9050..0000000000 --- a/libcloud/test/compute/fixtures/linode/_batch.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "ERRORARRAY": [], - "DATA": [ - { - "IPADDRESSID": 5384, - "RDNS_NAME": "li22-54.members.linode.com", - "LINODEID": 8098, - "ISPUBLIC": 1, - "IPADDRESS": "66.228.43.47" - }, - { - "IPADDRESSID": 5575, - "RDNS_NAME": "li22-245.members.linode.com", - "LINODEID": 8098, - "ISPUBLIC": 1, - "IPADDRESS": "75.127.96.245" - } - ], - "ACTION": "linode.ip.list" - } -] diff --git a/libcloud/test/compute/fixtures/linode/_linode_disk_list.json b/libcloud/test/compute/fixtures/linode/_linode_disk_list.json deleted file mode 100644 index 8ec05a9f1d..0000000000 --- a/libcloud/test/compute/fixtures/linode/_linode_disk_list.json +++ /dev/null @@ -1,28 +0,0 @@ -{ -"ERRORARRAY":[], -"ACTION":"linode.disk.list", -"DATA":[ - { - "UPDATE_DT":"2009-06-30 13:19:00.0", - "DISKID":55319, - "LABEL":"test label", - "TYPE":"ext3", - "LINODEID":8098, - "ISREADONLY":0, - "STATUS":1, - "CREATE_DT":"2008-04-04 10:08:06.0", - "SIZE":4096 - }, - { - "UPDATE_DT":"2009-07-18 12:53:043.0", - "DISKID":55320, - "LABEL":"256M Swap Image", - "TYPE":"swap", - "LINODEID":8098, - "ISREADONLY":0, - "STATUS":1, - "CREATE_DT":"2008-04-04 10:08:06.0", - "SIZE":256 - } - ] -} diff --git a/libcloud/test/compute/fixtures/linode/_linode_ip_list.json b/libcloud/test/compute/fixtures/linode/_linode_ip_list.json deleted file mode 100644 index 33a969ca68..0000000000 --- a/libcloud/test/compute/fixtures/linode/_linode_ip_list.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ACTION": "linode.ip.list", - "DATA": [ - { - "IPADDRESS": "66.228.43.47", - "IPADDRESSID": 5384, - "ISPUBLIC": 1, - "LINODEID": 8098, - "RDNS_NAME": "li22-54.members.linode.com" - }, - { - "IPADDRESS": "75.127.96.245", - "IPADDRESSID": 5575, - "ISPUBLIC": 1, - "LINODEID": 8098, - "RDNS_NAME": "li22-245.members.linode.com" - } - ], - "ERRORARRAY": [] -} diff --git a/libcloud/test/compute/fixtures/linode/_linode_list.json b/libcloud/test/compute/fixtures/linode/_linode_list.json deleted file mode 100644 index 345f7cadbb..0000000000 --- a/libcloud/test/compute/fixtures/linode/_linode_list.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "ALERT_CPU_ENABLED": 1, - "ALERT_BWIN_ENABLED": 1, - "ALERT_BWQUOTA_ENABLED": 1, - "BACKUPWINDOW": 0, - "ALERT_DISKIO_THRESHOLD": 1000, - "DISTRIBUTIONVENDOR": "Debian", - "WATCHDOG": 1, - "DATACENTERID": 6, - "STATUS": 1, - "ALERT_DISKIO_ENABLED": 1, - "CREATE_DT": "2012-05-04 19:31:30.0", - "TOTALHD": 49152, - "ALERT_BWQUOTA_THRESHOLD": 80, - "TOTALRAM": 2048, - "ALERT_BWIN_THRESHOLD": 5, - "LINODEID": 8098, - "ALERT_BWOUT_THRESHOLD": 5, - "ALERT_BWOUT_ENABLED": 1, - "BACKUPSENABLED": 1, - "ALERT_CPU_THRESHOLD": 90, - "PLANID": "2", - "BACKUPWEEKLYDAY": 0, - "LABEL": "api-node3", - "LPM_DISPLAYGROUP": "test", - "TOTALXFER": 3000 - } - ], - "ACTION": "linode.list" -} diff --git a/libcloud/test/compute/test_linode.py b/libcloud/test/compute/test_linode.py deleted file mode 100644 index bbe6354488..0000000000 --- a/libcloud/test/compute/test_linode.py +++ /dev/null @@ -1,208 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -# Maintainer: Jed Smith -# Based upon code written by Alex Polvi -# - -import sys -import unittest - -from libcloud.test import MockHttp -from libcloud.utils.py3 import httplib -from libcloud.compute.base import Node, StorageVolume, NodeAuthSSHKey, NodeAuthPassword -from libcloud.test.compute import TestCaseMixin -from libcloud.test.file_fixtures import ComputeFileFixtures -from libcloud.compute.drivers.linode import LinodeNodeDriver - - -class LinodeTest(unittest.TestCase, TestCaseMixin): - # The Linode test suite - - def setUp(self): - LinodeNodeDriver.connectionCls.conn_class = LinodeMockHttp - LinodeMockHttp.use_param = "api_action" - self.driver = LinodeNodeDriver("foo", api_version="3.0") - - def test_list_nodes(self): - nodes = self.driver.list_nodes() - self.assertEqual(len(nodes), 1) - node = nodes[0] - self.assertEqual(node.id, "8098") - self.assertEqual(node.name, "api-node3") - self.assertEqual(node.extra["PLANID"], "2") - self.assertTrue("75.127.96.245" in node.public_ips) - self.assertEqual(node.private_ips, []) - - def test_reboot_node(self): - # An exception would indicate failure - node = self.driver.list_nodes()[0] - self.driver.reboot_node(node) - - def test_destroy_node(self): - # An exception would indicate failure - node = self.driver.list_nodes()[0] - self.driver.destroy_node(node) - - def test_create_node_password_auth(self): - # Will exception on failure - self.driver.create_node( - name="Test", - location=self.driver.list_locations()[0], - size=self.driver.list_sizes()[0], - image=self.driver.list_images()[6], - auth=NodeAuthPassword("test123"), - ) - - def test_create_node_ssh_key_auth(self): - # Will exception on failure - node = self.driver.create_node( - name="Test", - location=self.driver.list_locations()[0], - size=self.driver.list_sizes()[0], - image=self.driver.list_images()[6], - auth=NodeAuthSSHKey("foo"), - ) - self.assertTrue(isinstance(node, Node)) - - def test_list_sizes(self): - sizes = self.driver.list_sizes() - self.assertEqual(len(sizes), 9) - for size in sizes: - self.assertEqual(size.ram, int(size.name.split(" ")[1])) - - def test_list_images(self): - images = self.driver.list_images() - self.assertEqual(len(images), 30) - - def test_create_node_response(self): - # should return a node object - node = self.driver.create_node( - name="node-name", - location=self.driver.list_locations()[0], - size=self.driver.list_sizes()[0], - image=self.driver.list_images()[0], - auth=NodeAuthPassword("foobar"), - ) - self.assertTrue(isinstance(node, Node)) - - def test_destroy_volume(self): - # Will exception on failure - node = self.driver.list_nodes()[0] - volume = StorageVolume( - id=55648, - name="test", - size=1024, - driver=self.driver, - extra={"LINODEID": node.id}, - ) - self.driver.destroy_volume(volume) - - def test_ex_create_volume(self): - # should return a StorageVolume object - node = self.driver.list_nodes()[0] - volume = self.driver.ex_create_volume( - size=4096, name="Another test image", node=node, fs_type="ext4" - ) - self.assertTrue(isinstance(volume, StorageVolume)) - - def test_ex_list_volumes(self): - # should return list of StorageVolume objects - node = self.driver.list_nodes()[0] - volumes = self.driver.ex_list_volumes(node=node) - - self.assertTrue(isinstance(volumes, list)) - self.assertTrue(isinstance(volumes[0], StorageVolume)) - self.assertEqual(len(volumes), 2) - - -class LinodeMockHttp(MockHttp): - fixtures = ComputeFileFixtures("linode") - - def _avail_datacenters(self, method, url, body, headers): - body = self.fixtures.load("_avail_datacenters.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _avail_linodeplans(self, method, url, body, headers): - body = self.fixtures.load("_avail_linodeplans.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _avail_distributions(self, method, url, body, headers): - body = self.fixtures.load("_avail_distributions.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_create(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.create","DATA":{"LinodeID":8098}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_create(self, method, url, body, headers): - body = ( - '{"ERRORARRAY":[],"ACTION":"linode.disk.create","DATA":{"JobID":1298,"DiskID":55647}}' - ) - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_delete(self, method, url, body, headers): - body = ( - '{"ERRORARRAY":[],"ACTION":"linode.disk.delete","DATA":{"JobID":1298,"DiskID":55648}}' - ) - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_createfromdistribution(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.disk.createFromDistribution","DATA":{"JobID":1298,"DiskID":55647}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_list(self, method, url, body, headers): - body = self.fixtures.load("_linode_disk_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_delete(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.delete","DATA":{"LinodeID":8098}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_update(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.update","DATA":{"LinodeID":8098}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_reboot(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.reboot","DATA":{"JobID":1305}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _avail_kernels(self, method, url, body, headers): - body = self.fixtures.load("_avail_kernels.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_boot(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.boot","DATA":{"JobID":1300}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_config_create(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.config.create","DATA":{"ConfigID":31239}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_list(self, method, url, body, headers): - body = self.fixtures.load("_linode_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_ip_list(self, method, url, body, headers): - body = self.fixtures.load("_linode_ip_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _batch(self, method, url, body, headers): - body = self.fixtures.load("_batch.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - -if __name__ == "__main__": - sys.exit(unittest.main()) diff --git a/libcloud/test/dns/fixtures/linode/create_domain.json b/libcloud/test/dns/fixtures/linode/create_domain.json deleted file mode 100644 index a9eef97a03..0000000000 --- a/libcloud/test/dns/fixtures/linode/create_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "domain.create", - "DATA": { - "DomainID": 5094 - } -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/create_domain_validation_error.json b/libcloud/test/dns/fixtures/linode/create_domain_validation_error.json deleted file mode 100644 index 3c7059724f..0000000000 --- a/libcloud/test/dns/fixtures/linode/create_domain_validation_error.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 8, - "ERRORMESSAGE": "The domain 'linode.com' already exists in our database. Please open a ticket if you think this is in error." - } - ], - "DATA": {}, - "ACTION": "domain.create" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/create_resource.json b/libcloud/test/dns/fixtures/linode/create_resource.json deleted file mode 100644 index 0fa3738e35..0000000000 --- a/libcloud/test/dns/fixtures/linode/create_resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "ResourceID": 3585100 - }, - "ACTION": "domain.resource.create" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_domain.json b/libcloud/test/dns/fixtures/linode/delete_domain.json deleted file mode 100644 index ff39a38ed7..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "domain.delete", - "DATA": { - "DomainID": 5123 - } -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json b/libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json deleted file mode 100644 index 8965baa6ee..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.delete" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_resource.json b/libcloud/test/dns/fixtures/linode/delete_resource.json deleted file mode 100644 index 7f7af2adbf..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "ResourceID": 3585141 - }, - "ACTION": "domain.resource.delete" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json b/libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json deleted file mode 100644 index b6969aa64c..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.resource.delete" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/domain_list.json b/libcloud/test/dns/fixtures/linode/domain_list.json deleted file mode 100644 index ac88b9b5ef..0000000000 --- a/libcloud/test/dns/fixtures/linode/domain_list.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "domain.list", - "DATA": [ - { - "DOMAINID": 5093, - "DESCRIPTION": "", - "EXPIRE_SEC": 0, - "RETRY_SEC": 0, - "STATUS": 1, - "LPM_DISPLAYGROUP": "thing", - "MASTER_IPS": "", - "REFRESH_SEC": 0, - "SOA_EMAIL": "dns@example.com", - "TTL_SEC": 0, - "DOMAIN": "linode.com", - "AXFR_IPS": "none", - "TYPE": "master" - }, - { - "DOMAINID": 5094, - "DESCRIPTION": "", - "EXPIRE_SEC": 0, - "RETRY_SEC": 0, - "STATUS": 1, - "LPM_DISPLAYGROUP": "", - "MASTER_IPS": "2600:3c03::f03c:91ff:feae:e071;66.228.43.47;", - "REFRESH_SEC": 0, - "SOA_EMAIL": "", - "TTL_SEC": 0, - "DOMAIN": "0.c.d.7.0.6.0.f.1.0.7.4.0.1.0.0.2.ip6.arpa", - "AXFR_IPS": "2600:3c03::f03c:91ff:feae:e071;66.228.43.47;", - "TYPE": "slave" - } - ] -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_record.json b/libcloud/test/dns/fixtures/linode/get_record.json deleted file mode 100644 index 4d5b0eb597..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_record.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "DOMAINID": 5093, - "PORT": 80, - "RESOURCEID": 3585100, - "NAME": "www", - "WEIGHT": 5, - "TTL_SEC": 0, - "TARGET": "127.0.0.1", - "PRIORITY": 10, - "PROTOCOL": "", - "TYPE": "a" - } - ], - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json b/libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json deleted file mode 100644 index c2c1fb499d..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_zone.json b/libcloud/test/dns/fixtures/linode/get_zone.json deleted file mode 100644 index e9387122f6..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_zone.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "DOMAINID": 5093, - "DESCRIPTION": "", - "EXPIRE_SEC": 0, - "RETRY_SEC": 0, - "STATUS": 1, - "LPM_DISPLAYGROUP": "thing", - "MASTER_IPS": "", - "REFRESH_SEC": 0, - "SOA_EMAIL": "dns@example.com", - "TTL_SEC": 0, - "DOMAIN": "linode.com", - "AXFR_IPS": "none", - "TYPE": "master" - } - ], - "ACTION": "domain.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json b/libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json deleted file mode 100644 index ea18547f51..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/resource_list.json b/libcloud/test/dns/fixtures/linode/resource_list.json deleted file mode 100644 index 2ed18cd821..0000000000 --- a/libcloud/test/dns/fixtures/linode/resource_list.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "DOMAINID": 5093, - "PORT": 80, - "RESOURCEID": 3585100, - "NAME": "mc", - "WEIGHT": 5, - "TTL_SEC": 0, - "TARGET": "127.0.0.1", - "PRIORITY": 10, - "PROTOCOL": "", - "TYPE": "a" - }, - { - "DOMAINID": 5093, - "PORT": 25565, - "RESOURCEID": 3585141, - "NAME": "_minecraft._udp", - "WEIGHT": 5, - "TTL_SEC": 0, - "TARGET": "mc.linode.com", - "PRIORITY": 10, - "PROTOCOL": "udp", - "TYPE": "srv" - } - ], - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json b/libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json deleted file mode 100644 index c2c1fb499d..0000000000 --- a/libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/update_domain.json b/libcloud/test/dns/fixtures/linode/update_domain.json deleted file mode 100644 index f695f4c103..0000000000 --- a/libcloud/test/dns/fixtures/linode/update_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "DomainID": 5093 - }, - "ACTION": "domain.update" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/update_resource.json b/libcloud/test/dns/fixtures/linode/update_resource.json deleted file mode 100644 index 7f88aabe6f..0000000000 --- a/libcloud/test/dns/fixtures/linode/update_resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "ResourceID": 3585100 - }, - "ACTION": "domain.resource.update" -} \ No newline at end of file diff --git a/libcloud/test/dns/test_linode.py b/libcloud/test/dns/test_linode.py deleted file mode 100644 index 7e6b3439b2..0000000000 --- a/libcloud/test/dns/test_linode.py +++ /dev/null @@ -1,316 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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 - -import sys -import unittest - -from libcloud.test import MockHttp -from libcloud.dns.types import RecordType, ZoneDoesNotExistError, RecordDoesNotExistError -from libcloud.utils.py3 import httplib -from libcloud.test.secrets import DNS_PARAMS_LINODE, DNS_KEYWORD_PARAMS_LINODE -from libcloud.common.linode import LinodeException -from libcloud.dns.drivers.linode import LinodeDNSDriver, LinodeDNSDriverV3 -from libcloud.test.file_fixtures import DNSFileFixtures - - -class LinodeTests(unittest.TestCase): - def setUp(self): - LinodeDNSDriverV3.connectionCls.conn_class = LinodeMockHttp - LinodeMockHttp.use_param = "api_action" - LinodeMockHttp.type = None - self.driver = LinodeDNSDriver(*DNS_PARAMS_LINODE, **DNS_KEYWORD_PARAMS_LINODE) - - def assertHasKeys(self, dictionary, keys): - for key in keys: - self.assertTrue(key in dictionary, 'key "%s" not in dictionary' % (key)) - - def test_list_record_types(self): - record_types = self.driver.list_record_types() - self.assertEqual(len(record_types), 7) - self.assertTrue(RecordType.A in record_types) - - def test_list_zones_success(self): - zones = self.driver.list_zones() - self.assertEqual(len(zones), 2) - - zone = zones[0] - self.assertEqual(zone.id, "5093") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "linode.com") - self.assertIsNone(zone.ttl) - self.assertHasKeys(zone.extra, ["description", "SOA_Email", "status"]) - - def test_list_records_success(self): - zone = self.driver.list_zones()[0] - records = self.driver.list_records(zone=zone) - self.assertEqual(len(records), 2) - - arecord = records[0] - self.assertEqual(arecord.id, "3585100") - self.assertEqual(arecord.name, "mc") - self.assertEqual(arecord.type, RecordType.A) - self.assertEqual(arecord.data, "127.0.0.1") - self.assertHasKeys(arecord.extra, ["protocol", "ttl_sec", "port", "weight"]) - - srvrecord = records[1] - self.assertEqual(srvrecord.id, "3585141") - self.assertEqual(srvrecord.name, "_minecraft._udp") - self.assertEqual(srvrecord.type, RecordType.SRV) - self.assertEqual(srvrecord.data, "mc.linode.com") - self.assertHasKeys(srvrecord.extra, ["protocol", "ttl_sec", "port", "priority", "weight"]) - - def test_list_records_zone_does_not_exist(self): - zone = self.driver.list_zones()[0] - - LinodeMockHttp.type = "ZONE_DOES_NOT_EXIST" - try: - self.driver.list_records(zone=zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, zone.id) - else: - self.fail("Exception was not thrown") - - def test_get_zone_success(self): - LinodeMockHttp.type = "GET_ZONE" - - zone = self.driver.get_zone(zone_id="5093") - self.assertEqual(zone.id, "5093") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "linode.com") - self.assertIsNone(zone.ttl) - self.assertHasKeys(zone.extra, ["description", "SOA_Email", "status"]) - - def test_get_zone_does_not_exist(self): - LinodeMockHttp.type = "GET_ZONE_DOES_NOT_EXIST" - - try: - self.driver.get_zone(zone_id="4444") - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, "4444") - else: - self.fail("Exception was not thrown") - - def test_get_record_success(self): - LinodeMockHttp.type = "GET_RECORD" - record = self.driver.get_record(zone_id="1234", record_id="3585100") - self.assertEqual(record.id, "3585100") - self.assertEqual(record.name, "www") - self.assertEqual(record.type, RecordType.A) - self.assertEqual(record.data, "127.0.0.1") - self.assertHasKeys(record.extra, ["protocol", "ttl_sec", "port", "weight"]) - - def test_get_record_zone_does_not_exist(self): - LinodeMockHttp.type = "GET_RECORD_ZONE_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="444", record_id="3585100") - except ZoneDoesNotExistError: - pass - else: - self.fail("Exception was not thrown") - - def test_get_record_record_does_not_exist(self): - LinodeMockHttp.type = "GET_RECORD_RECORD_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="4441", record_id="3585100") - except RecordDoesNotExistError: - pass - else: - self.fail("Exception was not thrown") - - def test_create_zone_success(self): - zone = self.driver.create_zone(domain="foo.bar.com", type="master", ttl=None, extra=None) - self.assertEqual(zone.id, "5094") - self.assertEqual(zone.domain, "foo.bar.com") - - def test_create_zone_validaton_error(self): - LinodeMockHttp.type = "VALIDATION_ERROR" - - try: - self.driver.create_zone(domain="foo.bar.com", type="master", ttl=None, extra=None) - except LinodeException: - pass - else: - self.fail("Exception was not thrown") - - def test_update_zone_success(self): - zone = self.driver.list_zones()[0] - updated_zone = self.driver.update_zone( - zone=zone, - domain="libcloud.org", - ttl=10, - extra={"SOA_Email": "bar@libcloud.org"}, - ) - - self.assertEqual(zone.extra["SOA_Email"], "dns@example.com") - - self.assertEqual(updated_zone.id, zone.id) - self.assertEqual(updated_zone.domain, "libcloud.org") - self.assertEqual(updated_zone.type, zone.type) - self.assertEqual(updated_zone.ttl, 10) - self.assertEqual(updated_zone.extra["SOA_Email"], "bar@libcloud.org") - self.assertEqual(updated_zone.extra["status"], zone.extra["status"]) - self.assertEqual(updated_zone.extra["description"], zone.extra["description"]) - - def test_create_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.create_record( - name="www", zone=zone, type=RecordType.A, data="127.0.0.1" - ) - - self.assertEqual(record.id, "3585100") - self.assertEqual(record.name, "www") - self.assertEqual(record.zone, zone) - self.assertEqual(record.type, RecordType.A) - self.assertEqual(record.data, "127.0.0.1") - - def test_update_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - updated_record = self.driver.update_record( - record=record, name="www", type=RecordType.AAAA, data="::1" - ) - - self.assertEqual(record.data, "127.0.0.1") - - self.assertEqual(updated_record.id, record.id) - self.assertEqual(updated_record.name, "www") - self.assertEqual(updated_record.zone, record.zone) - self.assertEqual(updated_record.type, RecordType.AAAA) - self.assertEqual(updated_record.data, "::1") - - def test_delete_zone_success(self): - zone = self.driver.list_zones()[0] - status = self.driver.delete_zone(zone=zone) - self.assertTrue(status) - - def test_delete_zone_does_not_exist(self): - zone = self.driver.list_zones()[0] - - LinodeMockHttp.type = "ZONE_DOES_NOT_EXIST" - - try: - self.driver.delete_zone(zone=zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, zone.id) - else: - self.fail("Exception was not thrown") - - def test_delete_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - status = self.driver.delete_record(record=record) - self.assertTrue(status) - - def test_delete_record_does_not_exist(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - - LinodeMockHttp.type = "RECORD_DOES_NOT_EXIST" - - try: - self.driver.delete_record(record=record) - except RecordDoesNotExistError as e: - self.assertEqual(e.record_id, record.id) - else: - self.fail("Exception was not thrown") - - -class LinodeMockHttp(MockHttp): - fixtures = DNSFileFixtures("linode") - - def _domain_list(self, method, url, body, headers): - body = self.fixtures.load("domain_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("resource_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _ZONE_DOES_NOT_EXIST_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("resource_list_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_ZONE_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_ZONE_DOES_NOT_EXIST_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("get_record.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_ZONE_DOES_NOT_EXIST_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_ZONE_DOES_NOT_EXIST_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("get_record_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_RECORD_DOES_NOT_EXIST_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_RECORD_DOES_NOT_EXIST_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("get_record_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_create(self, method, url, body, headers): - body = self.fixtures.load("create_domain.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _VALIDATION_ERROR_domain_create(self, method, url, body, headers): - body = self.fixtures.load("create_domain_validation_error.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_update(self, method, url, body, headers): - body = self.fixtures.load("update_domain.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_create(self, method, url, body, headers): - body = self.fixtures.load("create_resource.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_update(self, method, url, body, headers): - body = self.fixtures.load("update_resource.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_domain.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _ZONE_DOES_NOT_EXIST_domain_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_domain_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_resource.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _RECORD_DOES_NOT_EXIST_domain_resource_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_resource_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - -if __name__ == "__main__": - sys.exit(unittest.main()) From 0da5453e323d9fe037f67f972aafcb055d5b8fe3 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 11:43:36 -0500 Subject: [PATCH 37/52] dns: linode: Add API request checking --- libcloud/test/dns/test_linode_v4.py | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/libcloud/test/dns/test_linode_v4.py b/libcloud/test/dns/test_linode_v4.py index 3b461f74cd..7c319ee82d 100644 --- a/libcloud/test/dns/test_linode_v4.py +++ b/libcloud/test/dns/test_linode_v4.py @@ -27,6 +27,7 @@ class LinodeTests(unittest.TestCase): def setUp(self): LinodeDNSDriverV4.connectionCls.conn_class = LinodeMockHttpV4 LinodeMockHttpV4.type = None + LinodeMockHttpV4.history.clear() self.driver = LinodeDNSDriver(*DNS_PARAMS_LINODE) def test_correct_class_is_used(self): @@ -37,6 +38,11 @@ def test_unknown_api_version(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v4/domains") + self.assertEqual(len(zones), 3) zone = zones[0] self.assertEqual(zone.id, "123") @@ -47,6 +53,11 @@ def test_list_zones(self): def test_list_records(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone) + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v4/domains/123/records") + self.assertEqual(len(records), 6) record = records[0] self.assertEqual(record.id, "123") @@ -55,6 +66,11 @@ def test_list_records(self): def test_get_zone(self): zone = self.driver.get_zone("123") + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v4/domains/123") + self.assertEqual(zone.id, "123") self.assertEqual(zone.domain, "test.com") self.assertEqual(zone.extra["soa_email"], "admin@test.com") @@ -67,6 +83,13 @@ def test_get_zone_not_found(self): def test_get_record_A_RECORD(self): LinodeMockHttpV4.type = "A_RECORD" record = self.driver.get_record("123", "123") + + # [0] /v4/domains/123/records/123 + # [1] /v4/domains/123 + sent = LinodeMockHttpV4.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v4/domains/123/records/123") + self.assertEqual(record.id, "123") self.assertEqual(record.name, "test.example.com") self.assertEqual(record.type, "A") @@ -83,6 +106,14 @@ def test_create_zone(self): ttl = 300 extra = {"soa_email": "admin@example.com"} zone = self.driver.create_zone(domain=domain, ttl=ttl, extra=extra) + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v4/domains") + self.assertEqual(sent.json["domain"], domain) + self.assertEqual(sent.json["ttl_sec"], ttl) + self.assertEqual(sent.json["soa_email"], extra["soa_email"]) + self.assertEqual(zone.ttl, 300) self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.extra["soa_email"], "admin@example.com") @@ -93,6 +124,14 @@ def test_create_record(self): type = RecordType.A data = "200.150.100.50" record = self.driver.create_record(name, zone, type, data) + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v4/domains/123/records") + self.assertEqual(sent.json["name"], name) + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["target"], data) + self.assertEqual(record.id, "123") self.assertEqual(record.name, name) self.assertEqual(record.type, "A") @@ -104,6 +143,15 @@ def test_update_zone(self): ttl = 300 extra = {"description": "Testing", "soa_email": "admin@example.com"} updated_zone = self.driver.update_zone(zone, domain, ttl=ttl, extra=extra) + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v4/domains/123") + self.assertEqual(sent.json["domain"], domain) + self.assertEqual(sent.json["ttl_sec"], ttl) + self.assertEqual(sent.json["soa_email"], extra["soa_email"]) + self.assertEqual(sent.json["description"], extra["description"]) + self.assertEqual(updated_zone.domain, domain) self.assertEqual(updated_zone.ttl, ttl) self.assertEqual(updated_zone.extra["soa_email"], extra["soa_email"]) @@ -116,6 +164,14 @@ def test_update_record(self): data = "200.150.100.50" extra = {"ttl_sec": 3600} updated = self.driver.update_record(record, name=name, data=data, extra=extra) + + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/v4/domains/123/records/123") + self.assertEqual(sent.json["name"], name) + self.assertEqual(sent.json["target"], data) + self.assertEqual(sent.json["ttl_sec"], extra["ttl_sec"]) + self.assertEqual(updated.name, name) self.assertEqual(updated.ttl, extra["ttl_sec"]) self.assertEqual(updated.data, data) @@ -124,14 +180,23 @@ def test_delete_zone(self): zone = self.driver.list_zones()[0] self.assertTrue(self.driver.delete_zone(zone)) + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v4/domains/123") + def test_delete_record(self): zone = self.driver.list_zones()[0] record = self.driver.list_records(zone)[0] self.assertTrue(self.driver.delete_record(record)) + sent = LinodeMockHttpV4.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v4/domains/123/records/123") + class LinodeMockHttpV4(MockHttp): fixtures = DNSFileFixtures("linode_v4") + keep_history = True def _v4_domains(self, method, url, body, headers): if method == "GET": From 4cab9f7dcf32e341aafcf5c0b58a82b0e8828944 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 12:01:52 -0500 Subject: [PATCH 38/52] vultr: Drop deprecated v1 API support The v1 API became completely unsupported by Vultr on August 14, 2023. https://docs.vultr.com/vultr-api-v1-to-v2-transition-strategies --- libcloud/common/vultr.py | 89 ---- libcloud/compute/drivers/vultr.py | 332 +------------- libcloud/dns/drivers/vultr.py | 379 +--------------- .../fixtures/vultr/create_key_pair.json | 3 - .../compute/fixtures/vultr/create_node.json | 3 - .../fixtures/vultr/error_rate_limit.txt | 1 - .../compute/fixtures/vultr/list_images.json | 184 -------- .../fixtures/vultr/list_key_pairs.json | 8 - .../fixtures/vultr/list_locations.json | 172 -------- .../compute/fixtures/vultr/list_nodes.json | 116 ----- .../compute/fixtures/vultr/list_sizes.json | 405 ------------------ libcloud/test/compute/test_vultr.py | 236 ---------- .../test/dns/fixtures/vultr/delete_zone.json | 1 - .../fixtures/vultr/empty_records_list.json | 1 - .../dns/fixtures/vultr/empty_zones_list.json | 3 - .../test/dns/fixtures/vultr/get_record.json | 11 - .../test/dns/fixtures/vultr/get_zone.json | 7 - .../test/dns/fixtures/vultr/list_domains.json | 21 - .../test/dns/fixtures/vultr/list_records.json | 19 - .../test/dns/fixtures/vultr/test_zone.json | 9 - libcloud/test/dns/test_vultr.py | 310 -------------- 21 files changed, 4 insertions(+), 2306 deletions(-) delete mode 100644 libcloud/test/compute/fixtures/vultr/create_key_pair.json delete mode 100644 libcloud/test/compute/fixtures/vultr/create_node.json delete mode 100644 libcloud/test/compute/fixtures/vultr/error_rate_limit.txt delete mode 100644 libcloud/test/compute/fixtures/vultr/list_images.json delete mode 100644 libcloud/test/compute/fixtures/vultr/list_key_pairs.json delete mode 100644 libcloud/test/compute/fixtures/vultr/list_locations.json delete mode 100644 libcloud/test/compute/fixtures/vultr/list_nodes.json delete mode 100644 libcloud/test/compute/fixtures/vultr/list_sizes.json delete mode 100644 libcloud/test/compute/test_vultr.py delete mode 100644 libcloud/test/dns/fixtures/vultr/delete_zone.json delete mode 100644 libcloud/test/dns/fixtures/vultr/empty_records_list.json delete mode 100644 libcloud/test/dns/fixtures/vultr/empty_zones_list.json delete mode 100644 libcloud/test/dns/fixtures/vultr/get_record.json delete mode 100644 libcloud/test/dns/fixtures/vultr/get_zone.json delete mode 100644 libcloud/test/dns/fixtures/vultr/list_domains.json delete mode 100644 libcloud/test/dns/fixtures/vultr/list_records.json delete mode 100644 libcloud/test/dns/fixtures/vultr/test_zone.json delete mode 100644 libcloud/test/dns/test_vultr.py diff --git a/libcloud/common/vultr.py b/libcloud/common/vultr.py index 0415715217..242c572ecf 100644 --- a/libcloud/common/vultr.py +++ b/libcloud/common/vultr.py @@ -21,9 +21,7 @@ __all__ = [ "API_HOST", - "VultrConnection", "VultrException", - "VultrResponse", "DEFAULT_API_VERSION", "VultrResponseV2", "VultrConnectionV2", @@ -37,93 +35,6 @@ DEFAULT_API_VERSION = "2" -class VultrResponse(JsonResponse): - objects = None - error_dict = {} # type: Dict[str, str] - errors = None - ERROR_CODE_MAP = { - 400: "Invalid API location. Check the URL that you are using.", - 403: "Invalid or missing API key. Check that your API key is present" - + " and matches your assigned key.", - 405: "Invalid HTTP method. Check that the method (POST|GET) matches" - + " what the documentation indicates.", - 412: "Request failed. Check the response body for a more detailed" + " description.", - 500: "Internal server error. Try again at a later time.", - 503: "Rate limit hit. API requests are limited to an average of 1/s." - + " Try your request again later.", - } - - def __init__(self, response, connection): - self.errors = [] - super().__init__(response=response, connection=connection) - self.objects, self.errors = self.parse_body_and_errors() - if not self.success(): - raise self._make_excp(self.errors[0]) - - def parse_body_and_errors(self): - """ - Returns JSON data in a python list. - """ - json_objects = [] - errors = [] - - if self.status in self.ERROR_CODE_MAP: - self.error_dict["ERRORCODE"] = self.status - self.error_dict["ERRORMESSAGE"] = self.ERROR_CODE_MAP[self.status] - errors.append(self.error_dict) - - js = super().parse_body() - if isinstance(js, dict): - js = [js] - - json_objects.append(js) - - return (json_objects, errors) - - def _make_excp(self, error): - """ - Convert API error to a VultrException instance - """ - - return VultrException(error["ERRORCODE"], error["ERRORMESSAGE"]) - - def success(self): - return len(self.errors) == 0 - - -class VultrConnection(ConnectionKey): - """ - A connection to the Vultr API - """ - - host = API_HOST - responseCls = VultrResponse - - def add_default_params(self, params): - """ - Returns default params such as api_key which is - needed to perform an action.Returns a dictionary. - Example:/v1/server/upgrade_plan?api_key=self.key - """ - params["api_key"] = self.key - - return params - - def add_default_headers(self, headers): - """ - Returns default headers such as content-type. - Returns a dictionary. - """ - headers["Content-Type"] = "application/x-www-form-urlencoded" - headers["Accept"] = "text/plain" - - return headers - - def set_path(self): - self.path = "/v/" - return self.path - - class VultrResponseV2(JsonResponse): valid_response_codes = [ httplib.OK, diff --git a/libcloud/compute/drivers/vultr.py b/libcloud/compute/drivers/vultr.py index f07f0d2d02..db8ea02414 100644 --- a/libcloud/compute/drivers/vultr.py +++ b/libcloud/compute/drivers/vultr.py @@ -40,7 +40,6 @@ StorageVolume, ) from libcloud.compute.types import Provider, NodeState, StorageVolumeState, VolumeSnapshotState -from libcloud.utils.iso8601 import parse_date from libcloud.utils.publickey import get_pubkey_openssh_fingerprint # For matching region by id @@ -750,9 +749,7 @@ def __new__( **kwargs, ): if cls is VultrNodeDriver: - if api_version == "1": - cls = VultrNodeDriverV1 - elif api_version == "2": + if api_version == "2": cls = VultrNodeDriverV2 else: raise NotImplementedError( @@ -761,333 +758,6 @@ def __new__( return super().__new__(cls) -class VultrNodeDriverV1(VultrNodeDriver): - """ - VultrNode node driver. - """ - - connectionCls = VultrConnection - - NODE_STATE_MAP = {"pending": NodeState.PENDING, "active": NodeState.RUNNING} - - EX_CREATE_YES_NO_ATTRIBUTES = [ - "enable_ipv6", - "enable_private_network", - "auto_backups", - "notify_activate", - "ddos_protection", - ] - - EX_CREATE_ID_ATTRIBUTES = { - "iso_id": "ISOID", - "script_id": "SCRIPTID", - "snapshot_id": "SNAPSHOTID", - "app_id": "APPID", - } - - EX_CREATE_ATTRIBUTES = [ - "ipxe_chain_url", - "label", - "userdata", - "reserved_ip_v4", - "hostname", - "tag", - ] - EX_CREATE_ATTRIBUTES.extend(EX_CREATE_YES_NO_ATTRIBUTES) - EX_CREATE_ATTRIBUTES.extend(EX_CREATE_ID_ATTRIBUTES.keys()) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._helper = VultrNodeDriverHelper() - - def list_nodes(self): - return self._list_resources("/v1/server/list", self._to_node) - - def list_key_pairs(self): - """ - List all the available SSH keys. - :return: Available SSH keys. - :rtype: ``list`` of :class:`SSHKey` - """ - return self._list_resources("/v1/sshkey/list", self._to_ssh_key) - - def create_key_pair(self, name, public_key=""): - """ - Create a new SSH key. - :param name: Name of the new SSH key - :type name: ``str`` - - :key public_key: Public part of the new SSH key - :type name: ``str`` - - :return: True on success - :rtype: ``bool`` - """ - params = {"name": name, "ssh_key": public_key} - res = self.connection.post("/v1/sshkey/create", params) - return res.status == httplib.OK - - def delete_key_pair(self, key_pair): - """ - Delete an SSH key. - :param key_pair: The SSH key to delete - :type key_pair: :class:`SSHKey` - - :return: True on success - :rtype: ``bool`` - """ - params = {"SSHKEYID": key_pair.id} - res = self.connection.post("/v1/sshkey/destroy", params) - return res.status == httplib.OK - - def list_locations(self): - return self._list_resources("/v1/regions/list", self._to_location) - - def list_sizes(self): - return self._list_resources("/v1/plans/list", self._to_size) - - def list_images(self): - return self._list_resources("/v1/os/list", self._to_image) - - # pylint: disable=too-many-locals - def create_node(self, name, size, image, location, ex_ssh_key_ids=None, ex_create_attr=None): - """ - Create a node - - :param name: Name for the new node - :type name: ``str`` - - :param size: Size of the new node - :type size: :class:`NodeSize` - - :param image: Image for the new node - :type image: :class:`NodeImage` - - :param location: Location of the new node - :type location: :class:`NodeLocation` - - :param ex_ssh_key_ids: IDs of the SSH keys to initialize - :type ex_sshkeyid: ``list`` of ``str`` - - :param ex_create_attr: Extra attributes for node creation - :type ex_create_attr: ``dict`` - - The `ex_create_attr` parameter can include the following dictionary - key and value pairs: - - * `ipxe_chain_url`: ``str`` for specifying URL to boot via IPXE - * `iso_id`: ``str`` the ID of a specific ISO to mount, - only meaningful with the `Custom` `NodeImage` - * `script_id`: ``int`` ID of a startup script to execute on boot, - only meaningful when the `NodeImage` is not `Custom` - * 'snapshot_id`: ``str`` Snapshot ID to restore for the initial - installation, only meaningful with the `Snapshot` `NodeImage` - * `enable_ipv6`: ``bool`` Whether an IPv6 subnet should be assigned - * `enable_private_network`: ``bool`` Whether private networking - support should be added - * `label`: ``str`` Text label to be shown in the control panel - * `auto_backups`: ``bool`` Whether automatic backups should be enabled - * `app_id`: ``int`` App ID to launch if launching an application, - only meaningful when the `NodeImage` is `Application` - * `userdata`: ``str`` Base64 encoded cloud-init user-data - * `notify_activate`: ``bool`` Whether an activation email should be - sent when the server is ready - * `ddos_protection`: ``bool`` Whether DDOS protection should be enabled - * `reserved_ip_v4`: ``str`` IP address of the floating IP to use as - the main IP of this server - * `hostname`: ``str`` The hostname to assign to this server - * `tag`: ``str`` The tag to assign to this server - - :return: The newly created node. - :rtype: :class:`Node` - - """ - params = { - "DCID": location.id, - "VPSPLANID": size.id, - "OSID": image.id, - "label": name, - } - - if ex_ssh_key_ids is not None: - params["SSHKEYID"] = ",".join(ex_ssh_key_ids) - - ex_create_attr = ex_create_attr or {} - for key, value in ex_create_attr.items(): - if key in self.EX_CREATE_ATTRIBUTES: - if key in self.EX_CREATE_YES_NO_ATTRIBUTES: - params[key] = "yes" if value else "no" - else: - if key in self.EX_CREATE_ID_ATTRIBUTES: - key = self.EX_CREATE_ID_ATTRIBUTES[key] - params[key] = value - - result = self.connection.post("/v1/server/create", params) - if result.status != httplib.OK: - return False - - subid = result.object["SUBID"] - - retry_count = 3 - created_node = None - - for _ in range(retry_count): - try: - nodes = self.list_nodes() - created_node = [n for n in nodes if n.id == subid][0] - except IndexError: - time.sleep(1) - else: - break - - return created_node - - def reboot_node(self, node): - params = {"SUBID": node.id} - res = self.connection.post("/v1/server/reboot", params) - - return res.status == httplib.OK - - def destroy_node(self, node): - params = {"SUBID": node.id} - res = self.connection.post("/v1/server/destroy", params) - - return res.status == httplib.OK - - def _list_resources(self, url, tranform_func): - data = self.connection.get(url).object - sorted_key = sorted(data) - return [tranform_func(data[key]) for key in sorted_key] - - def _to_node(self, data): - if "status" in data: - state = self.NODE_STATE_MAP.get(data["status"], NodeState.UNKNOWN) - if state == NodeState.RUNNING and data["power_status"] != "running": - state = NodeState.STOPPED - else: - state = NodeState.UNKNOWN - - if "main_ip" in data and data["main_ip"] is not None: - public_ips = [data["main_ip"]] - else: - public_ips = [] - # simple check that we have ip address in value - if len(data["internal_ip"]) > 0: - private_ips = [data["internal_ip"]] - else: - private_ips = [] - created_at = parse_date(data["date_created"]) - - # response ordering - extra_keys = [ - "location", # Location name - "default_password", - "pending_charges", - "cost_per_month", - "current_bandwidth_gb", - "allowed_bandwidth_gb", - "netmask_v4", - "gateway_v4", - "power_status", - "server_state", - "v6_networks", - # TODO: Does we really need kvm_url? - "kvm_url", - "auto_backups", - "tag", - # "OSID", # Operating system to use. See v1/os/list. - "APPID", - "FIREWALLGROUPID", - ] - extra = self._helper.handle_extra(extra_keys, data) - - resolve_data = VULTR_COMPUTE_INSTANCE_IMAGES.get(data["OSID"]) - if resolve_data: - image = self._to_image(resolve_data) - else: - image = None - - resolve_data = VULTR_COMPUTE_INSTANCE_SIZES.get(data["VPSPLANID"]) - if resolve_data: - size = self._to_size(resolve_data) - else: - size = None - - # resolve_data = VULTR_COMPUTE_INSTANCE_LOCATIONS.get(data['DCID']) - # if resolve_data: - # location = self._to_location(resolve_data) - # extra['location'] = location - - node = Node( - id=data["SUBID"], - name=data["label"], - state=state, - public_ips=public_ips, - private_ips=private_ips, - image=image, - size=size, - extra=extra, - created_at=created_at, - driver=self, - ) - - return node - - def _to_location(self, data): - extra_keys = [ - "continent", - "state", - "ddos_protection", - "block_storage", - "regioncode", - ] - extra = self._helper.handle_extra(extra_keys, data) - - return NodeLocation( - id=data["DCID"], - name=data["name"], - country=data["country"], - extra=extra, - driver=self, - ) - - def _to_size(self, data): - extra_keys = [ - "vcpu_count", - "plan_type", - "available_locations", - ] - extra = self._helper.handle_extra(extra_keys, data) - - # backward compatibility - if extra.get("vcpu_count").isdigit(): - extra["vcpu_count"] = int(extra["vcpu_count"]) - - ram = int(data["ram"]) - disk = int(data["disk"]) - # NodeSize accepted int instead float - bandwidth = int(float(data["bandwidth"])) - price = float(data["price_per_month"]) - return NodeSize( - id=data["VPSPLANID"], - name=data["name"], - ram=ram, - disk=disk, - bandwidth=bandwidth, - price=price, - extra=extra, - driver=self, - ) - - def _to_image(self, data): - extra_keys = ["arch", "family"] - extra = self._helper.handle_extra(extra_keys, data) - return NodeImage(id=data["OSID"], name=data["name"], extra=extra, driver=self) - - def _to_ssh_key(self, data): - return SSHKey(id=data["SSHKEYID"], name=data["name"], pub_key=data["ssh_key"]) - - class VultrNodeDriverV2(VultrNodeDriver): """ Vultr API v2 NodeDriver. diff --git a/libcloud/dns/drivers/vultr.py b/libcloud/dns/drivers/vultr.py index e0fae847f6..d7b330ce89 100644 --- a/libcloud/dns/drivers/vultr.py +++ b/libcloud/dns/drivers/vultr.py @@ -22,24 +22,17 @@ from libcloud.dns.types import ( Provider, RecordType, - ZoneDoesNotExistError, - ZoneAlreadyExistsError, - RecordDoesNotExistError, - RecordAlreadyExistsError, ) -from libcloud.utils.py3 import urlencode from libcloud.common.vultr import ( DEFAULT_API_VERSION, - VultrResponse, - VultrConnection, VultrResponseV2, VultrConnectionV2, ) __all__ = [ "ZoneRequiredException", - "VultrDNSResponse", - "VultrDNSConnection", + "VultrDNSResponseV2", + "VultrDNSConnectionV2", "VultrDNSDriver", ] @@ -48,14 +41,6 @@ class ZoneRequiredException(Exception): pass -class VultrDNSResponse(VultrResponse): - pass - - -class VultrDNSConnection(VultrConnection): - responseCls = VultrDNSResponse - - class VultrDNSResponseV2(VultrResponseV2): pass @@ -81,9 +66,7 @@ def __new__( **kwargs, ): if cls is VultrDNSDriver: - if api_version == "1": - cls = VultrDNSDriverV1 - elif api_version == "2": + if api_version == "2": cls = VultrDNSDriverV2 else: raise NotImplementedError( @@ -93,362 +76,6 @@ def __new__( return super().__new__(cls) -class VultrDNSDriverV1(VultrDNSDriver): - connectionCls = VultrDNSConnection - - RECORD_TYPE_MAP = { - RecordType.A: "A", - RecordType.AAAA: "AAAA", - RecordType.TXT: "TXT", - RecordType.CNAME: "CNAME", - RecordType.MX: "MX", - RecordType.NS: "NS", - RecordType.SRV: "SRV", - } - - def list_zones(self): - """ - Return a list of records for the provided zone. - - :param zone: Zone to list records for. - :type zone: :class:`Zone` - - :return: ``list`` of :class:`Record` - """ - action = "/v1/dns/list" - params = {"api_key": self.key} - response = self.connection.request(action=action, params=params) - zones = self._to_zones(response.objects[0]) - - return zones - - def list_records(self, zone): - """ - Returns a list of records for the provided zone. - - :param zone: zone to list records for - :type zone: `Zone` - - :rtype: list of :class: `Record` - """ - - if not isinstance(zone, Zone): - raise ZoneRequiredException("zone should be of type Zone") - - zones = self.list_zones() - - if not self.ex_zone_exists(zone.domain, zones): - raise ZoneDoesNotExistError(value="", driver=self, zone_id=zone.domain) - - action = "/v1/dns/records" - params = {"domain": zone.domain} - response = self.connection.request(action=action, params=params) - records = self._to_records(response.objects[0], zone=zone) - - return records - - def get_zone(self, zone_id): - """ - Returns a `Zone` instance. - - :param zone_id: name of the zone user wants to get. - :type zone_id: ``str`` - - :rtype: :class:`Zone` - """ - ret_zone = None - - action = "/v1/dns/list" - params = {"api_key": self.key} - response = self.connection.request(action=action, params=params) - zones = self._to_zones(response.objects[0]) - - if not self.ex_zone_exists(zone_id, zones): - raise ZoneDoesNotExistError(value=None, zone_id=zone_id, driver=self) - - for zone in zones: - if zone_id == zone.domain: - ret_zone = zone - - return ret_zone - - def get_record(self, zone_id, record_id): - """ - Returns a Record instance. - - :param zone_id: name of the required zone - :type zone_id: ``str`` - - :param record_id: ID of the required record - :type record_id: ``str`` - - :rtype: :class: `Record` - """ - ret_record = None - zone = self.get_zone(zone_id=zone_id) - records = self.list_records(zone=zone) - - if not self.ex_record_exists(record_id, records): - raise RecordDoesNotExistError(value="", driver=self, record_id=record_id) - - for record in records: - if record_id == record.id: - ret_record = record - - return ret_record - - def create_zone(self, domain, type="master", ttl=None, extra=None): - """ - Returns a `Zone` object. - - :param domain: Zone domain name, (e.g. example.com). - :type domain: ``str`` - - :param type: Zone type (master / slave). - :type type: ``str`` - - :param ttl: TTL for new records. (optional) - :type ttl: ``int`` - - :param extra: (optional) Extra attributes (driver specific). - (e.g. {'serverip':'127.0.0.1'}) - """ - extra = extra or {} - - if extra and extra.get("serverip"): - serverip = extra["serverip"] - else: - raise ValueError("Missing servertip key in extra") - - params = {"api_key": self.key} - data = urlencode({"domain": domain, "serverip": serverip}) - action = "/v1/dns/create_domain" - zones = self.list_zones() - - if self.ex_zone_exists(domain, zones): - raise ZoneAlreadyExistsError(value="", driver=self, zone_id=domain) - - self.connection.request(params=params, action=action, data=data, method="POST") - zone = Zone(id=domain, domain=domain, type=type, ttl=ttl, driver=self, extra=extra) - - return zone - - def create_record(self, name, zone, type, data, extra=None): - """ - Create a new record. - - :param name: Record name without the domain name (e.g. www). - Note: If you want to create a record for a base domain - name, you should specify empty string ('') for this - argument. - :type name: ``str`` - - :param zone: Zone where the requested record is created. - :type zone: :class:`Zone` - - :param type: DNS record type (A, AAAA, ...). - :type type: :class:`RecordType` - - :param data: Data for the record (depends on the record type). - :type data: ``str`` - - :param extra: Extra attributes (driver specific). (optional) - :type extra: ``dict`` - - :rtype: :class:`Record` - """ - extra = extra or {} - - ret_record = None - old_records_list = self.list_records(zone=zone) - # check if record already exists - # if exists raise RecordAlreadyExistsError - - for record in old_records_list: - if record.name == name and record.data == data: - raise RecordAlreadyExistsError(value="", driver=self, record_id=record.id) - - MX = self.RECORD_TYPE_MAP.get("MX") - SRV = self.RECORD_TYPE_MAP.get("SRV") - - if extra and extra.get("priority"): - priority = int(extra["priority"]) - else: - priority = None - - post_data = { - "domain": zone.domain, - "name": name, - "type": self.RECORD_TYPE_MAP.get(type), - "data": data, - } - - if type == MX or type == SRV: - if priority is None: - raise ValueError("Missing priority argument for MX record type") - post_data["priority"] = priority - - encoded_data = urlencode(post_data) - params = {"api_key": self.key} - action = "/v1/dns/create_record" - - self.connection.request(action=action, params=params, data=encoded_data, method="POST") - updated_zone_records = zone.list_records() - - for record in updated_zone_records: - if record.name == name and record.data == data: - ret_record = record - - return ret_record - - def delete_zone(self, zone): - """ - Delete a zone. - - Note: This will delete all the records belonging to this zone. - - :param zone: Zone to delete. - :type zone: :class:`Zone` - - :rtype: ``bool`` - """ - action = "/v1/dns/delete_domain" - params = {"api_key": self.key} - data = urlencode({"domain": zone.domain}) - zones = self.list_zones() - - if not self.ex_zone_exists(zone.domain, zones): - raise ZoneDoesNotExistError(value="", driver=self, zone_id=zone.domain) - - response = self.connection.request(params=params, action=action, data=data, method="POST") - - return response.status == 200 - - def delete_record(self, record): - """ - Delete a record. - - :param record: Record to delete. - :type record: :class:`Record` - - :rtype: ``bool`` - """ - action = "/v1/dns/delete_record" - params = {"api_key": self.key} - data = urlencode({"RECORDID": record.id, "domain": record.zone.domain}) - - zone_records = self.list_records(record.zone) - - if not self.ex_record_exists(record.id, zone_records): - raise RecordDoesNotExistError(value="", driver=self, record_id=record.id) - - response = self.connection.request(action=action, params=params, data=data, method="POST") - - return response.status == 200 - - def ex_zone_exists(self, zone_id, zones_list): - """ - Function to check if a `Zone` object exists. - - :param zone_id: Name of the `Zone` object. - :type zone_id: ``str`` - - :param zones_list: A list containing `Zone` objects - :type zones_list: ``list`` - - :rtype: Returns `True` or `False` - """ - - zone_ids = [] - - for zone in zones_list: - zone_ids.append(zone.domain) - - return zone_id in zone_ids - - def ex_record_exists(self, record_id, records_list): - """ - :param record_id: Name of the `Record` object. - :type record_id: ``str`` - - :param records_list: A list containing `Record` objects - :type records_list: ``list`` - - :rtype: ``bool`` - """ - record_ids = [] - - for record in records_list: - record_ids.append(record.id) - - return record_id in record_ids - - def _to_zone(self, item): - """ - Build an object `Zone` from the item dictionary - - :param item: item to build the zone from - :type item: `dictionary` - - :rtype: :instance: `Zone` - """ - type = "master" - extra = {"date_created": item["date_created"]} - - zone = Zone( - id=item["domain"], - domain=item["domain"], - driver=self, - type=type, - ttl=None, - extra=extra, - ) - - return zone - - def _to_zones(self, items): - """ - Returns a list of `Zone` objects. - - :param: items: a list that contains dictionary objects to be passed - to the _to_zone function. - :type items: ``list`` - """ - zones = [] - - for item in items: - zones.append(self._to_zone(item)) - - return zones - - def _to_record(self, item, zone): - extra = {} - - if item.get("priority"): - extra["priority"] = item["priority"] - - type = self._string_to_record_type(item["type"]) - record = Record( - id=item["RECORDID"], - name=item["name"], - type=type, - data=item["data"], - zone=zone, - driver=self, - extra=extra, - ) - - return record - - def _to_records(self, items, zone): - records = [] - - for item in items: - records.append(self._to_record(item, zone=zone)) - - return records - - class VultrDNSDriverV2(VultrDNSDriver): connectionCls = VultrDNSConnectionV2 diff --git a/libcloud/test/compute/fixtures/vultr/create_key_pair.json b/libcloud/test/compute/fixtures/vultr/create_key_pair.json deleted file mode 100644 index f43bc0ae7b..0000000000 --- a/libcloud/test/compute/fixtures/vultr/create_key_pair.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "SSHKEYID": "5806ab4970aba" -} diff --git a/libcloud/test/compute/fixtures/vultr/create_node.json b/libcloud/test/compute/fixtures/vultr/create_node.json deleted file mode 100644 index 2a58f23a5d..0000000000 --- a/libcloud/test/compute/fixtures/vultr/create_node.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "SUBID": "41326859" -} diff --git a/libcloud/test/compute/fixtures/vultr/error_rate_limit.txt b/libcloud/test/compute/fixtures/vultr/error_rate_limit.txt deleted file mode 100644 index 27def76b8a..0000000000 --- a/libcloud/test/compute/fixtures/vultr/error_rate_limit.txt +++ /dev/null @@ -1 +0,0 @@ -Rate limit reached - please try your request again later. Current rate limit: 2 requests/sec \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/vultr/list_images.json b/libcloud/test/compute/fixtures/vultr/list_images.json deleted file mode 100644 index 45a35961ce..0000000000 --- a/libcloud/test/compute/fixtures/vultr/list_images.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "127": { - "OSID": 127, - "name": "CentOS 6 x64", - "arch": "x64", - "family": "centos", - "windows": false - }, - "147": { - "OSID": 147, - "name": "CentOS 6 i386", - "arch": "i386", - "family": "centos", - "windows": false - }, - "167": { - "OSID": 167, - "name": "CentOS 7 x64", - "arch": "x64", - "family": "centos", - "windows": false - }, - "381": { - "OSID": 381, - "name": "CentOS 7 SELinux x64", - "arch": "x64", - "family": "centos", - "windows": false - }, - "362": { - "OSID": 362, - "name": "CentOS 8 x64", - "arch": "x64", - "family": "centos", - "windows": false - }, - "401": { - "OSID": 401, - "name": "CentOS 8 Stream x64", - "arch": "x64", - "family": "centos", - "windows": false - }, - "215": { - "OSID": 215, - "name": "Ubuntu 16.04 x64", - "arch": "x64", - "family": "ubuntu", - "windows": false - }, - "216": { - "OSID": 216, - "name": "Ubuntu 16.04 i386", - "arch": "i386", - "family": "ubuntu", - "windows": false - }, - "270": { - "OSID": 270, - "name": "Ubuntu 18.04 x64", - "arch": "x64", - "family": "ubuntu", - "windows": false - }, - "387": { - "OSID": 387, - "name": "Ubuntu 20.04 x64", - "arch": "x64", - "family": "ubuntu", - "windows": false - }, - "194": { - "OSID": 194, - "name": "Debian 8 i386 (jessie)", - "arch": "i386", - "family": "debian", - "windows": false - }, - "244": { - "OSID": 244, - "name": "Debian 9 x64 (stretch)", - "arch": "x64", - "family": "debian", - "windows": false - }, - "352": { - "OSID": 352, - "name": "Debian 10 x64 (buster)", - "arch": "x64", - "family": "debian", - "windows": false - }, - "230": { - "OSID": 230, - "name": "FreeBSD 11 x64", - "arch": "x64", - "family": "freebsd", - "windows": false - }, - "327": { - "OSID": 327, - "name": "FreeBSD 12 x64", - "arch": "x64", - "family": "freebsd", - "windows": false - }, - "366": { - "OSID": 366, - "name": "OpenBSD 6.6 x64", - "arch": "x64", - "family": "openbsd", - "windows": false - }, - "394": { - "OSID": 394, - "name": "OpenBSD 6.7 x64", - "arch": "x64", - "family": "openbsd", - "windows": false - }, - "391": { - "OSID": 391, - "name": "Fedora CoreOS", - "arch": "x64", - "family": "fedora-coreos", - "windows": false - }, - "367": { - "OSID": 367, - "name": "Fedora 31 x64", - "arch": "x64", - "family": "fedora", - "windows": false - }, - "389": { - "OSID": 389, - "name": "Fedora 32 x64", - "arch": "x64", - "family": "fedora", - "windows": false - }, - "124": { - "OSID": 124, - "name": "Windows 2012 R2 x64", - "arch": "x64", - "family": "windows", - "windows": true - }, - "240": { - "OSID": 240, - "name": "Windows 2016 x64", - "arch": "x64", - "family": "windows", - "windows": true - }, - "159": { - "OSID": 159, - "name": "Custom", - "arch": "x64", - "family": "iso", - "windows": false - }, - "164": { - "OSID": 164, - "name": "Snapshot", - "arch": "x64", - "family": "snapshot", - "windows": false - }, - "180": { - "OSID": 180, - "name": "Backup", - "arch": "x64", - "family": "backup", - "windows": false - }, - "186": { - "OSID": 186, - "name": "Application", - "arch": "x64", - "family": "application", - "windows": false - } -} diff --git a/libcloud/test/compute/fixtures/vultr/list_key_pairs.json b/libcloud/test/compute/fixtures/vultr/list_key_pairs.json deleted file mode 100644 index b8d5cad19e..0000000000 --- a/libcloud/test/compute/fixtures/vultr/list_key_pairs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "5806a8ef2a0c6": { - "SSHKEYID": "5806a8ef2a0c6", - "date_created": "2016-10-18 18:57:51", - "name": "test-key-pair", - "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZYMivN4KqJZ3dNEWeH20PUeB2ZnZRkk91K5SgxWrEotgpX4pMVM/9oxkh4bKw5CBzT6KAOghzLcBViFpNVjDyyet9wwVcy6cjuUynx63UtbTLB+r4D+bD/+/9rQTeckvGYg9Y8xIKL/oaVeCcdBM8JhSQZbZ/aARi2K79FWGH61azAqc/JCHT63f3FhspjdVpcVoVOjsZG3WG6Vymys2cXH1PM5qMgBbmp+5LkSv0LvUULyxcrtKkUyntPr1BvIFSNbo2lhXLwnM4DXONP6U/yMFte+ZwiajF7pUCdB9HqvXVU+IfswYSDuhHzL9j8+ZLQ2enF/lkkYxpMHE2t215 tester@test" - } -} diff --git a/libcloud/test/compute/fixtures/vultr/list_locations.json b/libcloud/test/compute/fixtures/vultr/list_locations.json deleted file mode 100644 index 63a6194a3b..0000000000 --- a/libcloud/test/compute/fixtures/vultr/list_locations.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "6": { - "DCID": "6", - "name": "Atlanta", - "country": "US", - "continent": "North America", - "state": "GA", - "ddos_protection": false, - "block_storage": false, - "regioncode": "ATL" - }, - "2": { - "DCID": "2", - "name": "Chicago", - "country": "US", - "continent": "North America", - "state": "IL", - "ddos_protection": true, - "block_storage": false, - "regioncode": "ORD" - }, - "3": { - "DCID": "3", - "name": "Dallas", - "country": "US", - "continent": "North America", - "state": "TX", - "ddos_protection": true, - "block_storage": false, - "regioncode": "DFW" - }, - "5": { - "DCID": "5", - "name": "Los Angeles", - "country": "US", - "continent": "North America", - "state": "CA", - "ddos_protection": true, - "block_storage": false, - "regioncode": "LAX" - }, - "39": { - "DCID": "39", - "name": "Miami", - "country": "US", - "continent": "North America", - "state": "FL", - "ddos_protection": true, - "block_storage": false, - "regioncode": "MIA" - }, - "1": { - "DCID": "1", - "name": "New Jersey", - "country": "US", - "continent": "North America", - "state": "NJ", - "ddos_protection": true, - "block_storage": true, - "regioncode": "EWR" - }, - "4": { - "DCID": "4", - "name": "Seattle", - "country": "US", - "continent": "North America", - "state": "WA", - "ddos_protection": true, - "block_storage": false, - "regioncode": "SEA" - }, - "12": { - "DCID": "12", - "name": "Silicon Valley", - "country": "US", - "continent": "North America", - "state": "CA", - "ddos_protection": true, - "block_storage": false, - "regioncode": "SJC" - }, - "40": { - "DCID": "40", - "name": "Singapore", - "country": "SG", - "continent": "Asia", - "state": "", - "ddos_protection": false, - "block_storage": false, - "regioncode": "SGP" - }, - "7": { - "DCID": "7", - "name": "Amsterdam", - "country": "NL", - "continent": "Europe", - "state": "", - "ddos_protection": true, - "block_storage": false, - "regioncode": "AMS" - }, - "34": { - "DCID": "34", - "name": "Seoul", - "country": "KR", - "continent": "Asia", - "state": "", - "ddos_protection": false, - "block_storage": false, - "regioncode": "ICN" - }, - "25": { - "DCID": "25", - "name": "Tokyo", - "country": "JP", - "continent": "Asia", - "state": "", - "ddos_protection": false, - "block_storage": false, - "regioncode": "NRT" - }, - "8": { - "DCID": "8", - "name": "London", - "country": "GB", - "continent": "Europe", - "state": "", - "ddos_protection": true, - "block_storage": false, - "regioncode": "LHR" - }, - "24": { - "DCID": "24", - "name": "Paris", - "country": "FR", - "continent": "Europe", - "state": "", - "ddos_protection": true, - "block_storage": false, - "regioncode": "CDG" - }, - "9": { - "DCID": "9", - "name": "Frankfurt", - "country": "DE", - "continent": "Europe", - "state": "", - "ddos_protection": true, - "block_storage": false, - "regioncode": "FRA" - }, - "22": { - "DCID": "22", - "name": "Toronto", - "country": "CA", - "continent": "North America", - "state": "", - "ddos_protection": false, - "block_storage": false, - "regioncode": "YTO" - }, - "19": { - "DCID": "19", - "name": "Sydney", - "country": "AU", - "continent": "Australia", - "state": "", - "ddos_protection": false, - "block_storage": false, - "regioncode": "SYD" - } -} diff --git a/libcloud/test/compute/fixtures/vultr/list_nodes.json b/libcloud/test/compute/fixtures/vultr/list_nodes.json deleted file mode 100644 index 2648f5e404..0000000000 --- a/libcloud/test/compute/fixtures/vultr/list_nodes.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "41326859": { - "SUBID": "41326859", - "os": "CentOS SELinux 8 x64", - "ram": "1024 MB", - "disk": "Virtual 25 GB", - "main_ip": "217.69.11.158", - "vcpu_count": "1", - "location": "Paris", - "DCID": "24", - "default_password": "*7j6j6[#q", - "date_created": "2020-10-15 03:17:22", - "pending_charges": "0.04", - "status": "active", - "cost_per_month": "5.00", - "current_bandwidth_gb": 0, - "allowed_bandwidth_gb": "1000", - "netmask_v4": "255.255.254.0", - "gateway_v4": "217.69.10.1", - "power_status": "running", - "server_state": "locked", - "VPSPLANID": "201", - "v6_main_ip": "2001:19f0:6801:1cc8:5400:03ff:fe03:5478", - "v6_network_size": "64", - "v6_network": "2001:19f0:6801:1cc8::", - "v6_networks": [ - { - "v6_main_ip": "2001:19f0:6801:1cc8:5400:03ff:fe03:5478", - "v6_network_size": "64", - "v6_network": "2001:19f0:6801:1cc8::" - } - ], - "label": "labelname", - "internal_ip": "10.24.96.3", - "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV", - "auto_backups": "yes", - "tag": "", - "OSID": "362", - "APPID": "0", - "FIREWALLGROUPID": "0" - }, - "41306569": { - "SUBID": "41306569", - "os": "Ubuntu 20.04 x64", - "ram": "1024 MB", - "disk": "Virtual 25 GB", - "main_ip": "45.76.43.87", - "vcpu_count": "1", - "location": "Amsterdam", - "DCID": "7", - "default_password": "h6*hrte6tg", - "date_created": "2020-10-14 09:37:50", - "pending_charges": "0.18", - "status": "active", - "cost_per_month": "5.00", - "current_bandwidth_gb": 0.026, - "allowed_bandwidth_gb": "1000", - "netmask_v4": "255.255.254.0", - "gateway_v4": "45.76.42.1", - "power_status": "running", - "server_state": "installingbooting", - "VPSPLANID": "201", - "v6_main_ip": "2001:19f0:5001:2b9c:5400:03ff:fe03:9568", - "v6_network_size": "64", - "v6_network": "2001:19f0:5001:2b9c::", - "v6_networks": [ - { - "v6_main_ip": "2001:19f0:5001:2b9c:5400:03ff:fe03:9568", - "v6_network_size": "64", - "v6_network": "2001:19f0:5001:2b9c::" - } - ], - "label": "libcloud-label", - "internal_ip": "10.7.96.85", - "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=erewawxFVZw2mXnhGUV", - "auto_backups": "yes", - "tag": "Web", - "OSID": "387", - "APPID": "0", - "FIREWALLGROUPID": "0" - }, - "41326895": { - "SUBID": "41326895", - "os": "Ubuntu 18.04 x64", - "ram": "2048 MB", - "disk": "Virtual 55 GB", - "main_ip": "136.244.113.89", - "vcpu_count": "1", - "location": "Paris", - "DCID": "24", - "default_password": "Dy4@K_Z1gvb!!8zw", - "date_created": "2020-10-15 03:17:54", - "pending_charges": "0.00", - "status": "pending", - "cost_per_month": "10.00", - "current_bandwidth_gb": 0, - "allowed_bandwidth_gb": "2000", - "netmask_v4": "255.255.254.0", - "gateway_v4": "136.244.112.1", - "power_status": "running", - "server_state": "none", - "VPSPLANID": "202", - "v6_main_ip": false, - "v6_network_size": "", - "v6_network": "", - "v6_networks": [], - "label": "servlabel", - "internal_ip": "", - "kvm_url": "", - "auto_backups": "no", - "tag": "", - "OSID": "270", - "APPID": "0", - "FIREWALLGROUPID": "4e83489b" - } -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/vultr/list_sizes.json b/libcloud/test/compute/fixtures/vultr/list_sizes.json deleted file mode 100644 index cb786dab0b..0000000000 --- a/libcloud/test/compute/fixtures/vultr/list_sizes.json +++ /dev/null @@ -1,405 +0,0 @@ -{ - "201": { - "VPSPLANID": "201", - "name": "1024 MB RAM,25 GB SSD,1.00 TB BW", - "vcpu_count": "1", - "ram": "1024", - "disk": "25", - "bandwidth": "1.00", - "bandwidth_gb": "1024", - "price_per_month": "5.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "202": { - "VPSPLANID": "202", - "name": "2048 MB RAM,55 GB SSD,2.00 TB BW", - "vcpu_count": "1", - "ram": "2048", - "disk": "55", - "bandwidth": "2.00", - "bandwidth_gb": "2048", - "price_per_month": "10.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "203": { - "VPSPLANID": "203", - "name": "4096 MB RAM,80 GB SSD,3.00 TB BW", - "vcpu_count": "2", - "ram": "4096", - "disk": "80", - "bandwidth": "3.00", - "bandwidth_gb": "3072", - "price_per_month": "20.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "204": { - "VPSPLANID": "204", - "name": "8192 MB RAM,160 GB SSD,4.00 TB BW", - "vcpu_count": "4", - "ram": "8192", - "disk": "160", - "bandwidth": "4.00", - "bandwidth_gb": "4096", - "price_per_month": "40.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "205": { - "VPSPLANID": "205", - "name": "16384 MB RAM,320 GB SSD,5.00 TB BW", - "vcpu_count": "6", - "ram": "16384", - "disk": "320", - "bandwidth": "5.00", - "bandwidth_gb": "5120", - "price_per_month": "80.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "206": { - "VPSPLANID": "206", - "name": "32768 MB RAM,640 GB SSD,6.00 TB BW", - "vcpu_count": "8", - "ram": "32768", - "disk": "640", - "bandwidth": "6.00", - "bandwidth_gb": "6144", - "price_per_month": "160.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 39 - ] - }, - "207": { - "VPSPLANID": "207", - "name": "65536 MB RAM,1280 GB SSD,10.00 TB BW", - "vcpu_count": "16", - "ram": "65536", - "disk": "1280", - "bandwidth": "10.00", - "bandwidth_gb": "10240", - "price_per_month": "320.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 2, - 3, - 5, - 7, - 12, - 19, - 24, - 39 - ] - }, - "208": { - "VPSPLANID": "208", - "name": "98304 MB RAM,1600 GB SSD,15.00 TB BW", - "vcpu_count": "24", - "ram": "98304", - "disk": "1600", - "bandwidth": "15.00", - "bandwidth_gb": "15360", - "price_per_month": "640.00", - "plan_type": "SSD", - "windows": false, - "available_locations": [ - 12 - ] - }, - "115": { - "VPSPLANID": "115", - "name": "8192 MB RAM,110 GB SSD,10.00 TB BW", - "vcpu_count": "2", - "ram": "8192", - "disk": "110", - "bandwidth": "10.00", - "bandwidth_gb": "10240", - "price_per_month": "60.00", - "plan_type": "DEDICATED", - "windows": false, - "available_locations": [ - 1, - 12, - 25 - ] - }, - "116": { - "VPSPLANID": "116", - "name": "16384 MB RAM,2x110 GB SSD,20.00 TB BW", - "vcpu_count": "4", - "ram": "16384", - "disk": "110", - "bandwidth": "20.00", - "bandwidth_gb": "20480", - "price_per_month": "120.00", - "plan_type": "DEDICATED", - "windows": false, - "available_locations": [ - 1 - ] - }, - "117": { - "VPSPLANID": "117", - "name": "24576 MB RAM,3x110 GB SSD,30.00 TB BW", - "vcpu_count": "6", - "ram": "24576", - "disk": "110", - "bandwidth": "30.00", - "bandwidth_gb": "30720", - "price_per_month": "180.00", - "plan_type": "DEDICATED", - "windows": false, - "available_locations": [ - 1 - ] - }, - "118": { - "VPSPLANID": "118", - "name": "32768 MB RAM,4x110 GB SSD,40.00 TB BW", - "vcpu_count": "8", - "ram": "32768", - "disk": "110", - "bandwidth": "40.00", - "bandwidth_gb": "40960", - "price_per_month": "240.00", - "plan_type": "DEDICATED", - "windows": false, - "available_locations": [ - 1 - ] - }, - "400": { - "VPSPLANID": "400", - "name": "1024 MB RAM,32 GB SSD,1.00 TB BW", - "vcpu_count": "1", - "ram": "1024", - "disk": "32", - "bandwidth": "1.00", - "bandwidth_gb": "1024", - "price_per_month": "6.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "401": { - "VPSPLANID": "401", - "name": "2048 MB RAM,64 GB SSD,2.00 TB BW", - "vcpu_count": "1", - "ram": "2048", - "disk": "64", - "bandwidth": "2.00", - "bandwidth_gb": "2048", - "price_per_month": "12.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 12, - 19, - 22, - 24, - 25, - 39, - 40 - ] - }, - "402": { - "VPSPLANID": "402", - "name": "4096 MB RAM,128 GB SSD,3.00 TB BW", - "vcpu_count": "2", - "ram": "4096", - "disk": "128", - "bandwidth": "3.00", - "bandwidth_gb": "3072", - "price_per_month": "24.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [ - 1, - 4, - 5, - 7, - 12, - 19, - 22, - 24 - ] - }, - "403": { - "VPSPLANID": "403", - "name": "8192 MB RAM,256 GB SSD,4.00 TB BW", - "vcpu_count": "3", - "ram": "8192", - "disk": "256", - "bandwidth": "4.00", - "bandwidth_gb": "4096", - "price_per_month": "48.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [ - 1, - 4, - 22 - ] - }, - "404": { - "VPSPLANID": "404", - "name": "16384 MB RAM,384 GB SSD,5.00 TB BW", - "vcpu_count": "4", - "ram": "16384", - "disk": "384", - "bandwidth": "5.00", - "bandwidth_gb": "5120", - "price_per_month": "96.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [ - 22 - ] - }, - "405": { - "VPSPLANID": "405", - "name": "32768 MB RAM,512 GB SSD,6.00 TB BW", - "vcpu_count": "8", - "ram": "32768", - "disk": "512", - "bandwidth": "6.00", - "bandwidth_gb": "6144", - "price_per_month": "192.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [] - }, - "406": { - "VPSPLANID": "406", - "name": "49152 MB RAM,768 GB SSD,8.00 TB BW", - "vcpu_count": "12", - "ram": "49152", - "disk": "768", - "bandwidth": "8.00", - "bandwidth_gb": "8192", - "price_per_month": "256.00", - "plan_type": "HIGHFREQUENCY", - "windows": false, - "available_locations": [] - } -} diff --git a/libcloud/test/compute/test_vultr.py b/libcloud/test/compute/test_vultr.py deleted file mode 100644 index e2ac3f3751..0000000000 --- a/libcloud/test/compute/test_vultr.py +++ /dev/null @@ -1,236 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -import sys -import unittest - -from libcloud.test import MockHttp, LibcloudTestCase -from libcloud.utils.py3 import httplib -from libcloud.common.types import ServiceUnavailableError -from libcloud.compute.base import NodeSize, NodeImage -from libcloud.test.secrets import VULTR_PARAMS -from libcloud.test.file_fixtures import ComputeFileFixtures -from libcloud.compute.drivers.vultr import VultrNodeDriver, VultrNodeDriverV1 - -try: - import simplejson as json # pylint: disable=unused-import -except ImportError: - # pylint: disable=unused-import - import json # NOQA - - -# class VultrTests(unittest.TestCase, TestCaseMixin): -class VultrTests(LibcloudTestCase): - def setUp(self): - VultrNodeDriver.connectionCls.conn_class = VultrMockHttp - VultrMockHttp.type = None - self.driver = VultrNodeDriver(*VULTR_PARAMS, api_version="1") - - def test_correct_class_is_used(self): - self.assertIsInstance(self.driver, VultrNodeDriverV1) - - def test_list_images_dont_require_api_key(self): - self.driver.list_images() - self.assertFalse(self.driver.connection.require_api_key()) - - def test_list_images_success(self): - images = self.driver.list_images() - self.assertTrue(len(images) >= 1) - - image = images[0] - self.assertTrue(image.id is not None) - self.assertTrue(image.name is not None) - - def test_list_sizes_dont_require_api_key(self): - self.driver.list_sizes() - self.assertFalse(self.driver.connection.require_api_key()) - - def test_list_sizes_success(self): - """count of current plans""" - sizes = self.driver.list_sizes() - self.assertTrue(len(sizes) == 19) - - size = sizes[0] - self.assertTrue(size.id.isdigit()) - self.assertEqual(size.name, "8192 MB RAM,110 GB SSD,10.00 TB BW") - self.assertEqual(size.ram, 8192) - - size = sizes[16] - self.assertTrue(size.id.isdigit()) - self.assertEqual(size.name, "16384 MB RAM,384 GB SSD,5.00 TB BW") - self.assertEqual(size.ram, 16384) - - def test_list_locations_dont_require_api_key(self): - self.driver.list_locations() - self.assertFalse(self.driver.connection.require_api_key()) - - def test_list_locations_success(self): - locations = self.driver.list_locations() - self.assertTrue(len(locations) >= 1) - - location = locations[0] - self.assertEqual(location.id, "1") - self.assertEqual(location.name, "New Jersey") - self.assertEqual(location.extra["continent"], "North America") - - def test_list_locations_extra_success(self): - locations = self.driver.list_locations() - self.assertTrue(len(locations) >= 1) - extra_keys = [ - "continent", - "state", - "ddos_protection", - "block_storage", - "regioncode", - ] - for location in locations: - self.assertTrue(len(location.extra.keys()) >= 5) - self.assertTrue(all(item in location.extra.keys() for item in extra_keys)) - - def test_list_nodes_require_api_key(self): - self.driver.list_nodes() - self.assertTrue(self.driver.connection.require_api_key()) - - def test_list_nodes_success(self): - nodes = self.driver.list_nodes() - self.assertEqual(len(nodes), 3) - self.assertTrue(nodes[0].id.isdigit()) - self.assertEqual(nodes[0].id, "41306569") - self.assertEqual(nodes[0].public_ips, ["45.76.43.87"]) - self.assertEqual(nodes[0].private_ips, ["10.7.96.85"]) - self.assertEqual(nodes[2].private_ips, []) - - def test_list_nodes_image_success(self): - nodes = self.driver.list_nodes() - node = nodes[0] - self.assertTrue(isinstance(node.image, NodeImage)) - - def test_list_nodes_size_success(self): - nodes = self.driver.list_nodes() - node = nodes[0] - self.assertTrue(isinstance(node.size, NodeSize)) - - def test_list_nodes_success_extra(self): - extra_keys = [ - "default_password", - "pending_charges", - "cost_per_month", - ] - nodes = self.driver.list_nodes() - for node in nodes: - self.assertTrue(len(node.extra.keys()) > 5) - self.assertTrue(all(item in node.extra.keys() for item in extra_keys)) - - def test_reboot_node_success(self): - node = self.driver.list_nodes()[0] - result = self.driver.reboot_node(node) - self.assertTrue(result) - - def test_create_node_success(self): - test_size = self.driver.list_sizes()[0] - test_image = self.driver.list_images()[0] - test_location = self.driver.list_locations()[0] - created_node = self.driver.create_node("test-node", test_size, test_image, test_location) - self.assertEqual(created_node.id, "41326859") - - def test_destroy_node_success(self): - node = self.driver.list_nodes()[0] - result = self.driver.destroy_node(node) - self.assertTrue(result) - - def test_list_key_pairs_success(self): - key_pairs = self.driver.list_key_pairs() - self.assertEqual(len(key_pairs), 1) - key_pair = key_pairs[0] - self.assertEqual(key_pair.id, "5806a8ef2a0c6") - self.assertEqual(key_pair.name, "test-key-pair") - - def test_create_key_pair_success(self): - res = self.driver.create_key_pair("test-key-pair") - self.assertTrue(res) - - def test_delete_key_pair_success(self): - key_pairs = self.driver.list_key_pairs() - key_pair = key_pairs[0] - res = self.driver.delete_key_pair(key_pair) - self.assertTrue(res) - - def test_rate_limit(self): - VultrMockHttp.type = "SERVICE_UNAVAILABLE" - self.assertRaises(ServiceUnavailableError, self.driver.list_nodes) - - -class VultrMockHttp(MockHttp): - fixtures = ComputeFileFixtures("vultr") - - # pylint: disable=unused-argument - def _v1_regions_list(self, method, url, body, headers): - body = self.fixtures.load("list_locations.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_os_list(self, method, url, body, headers): - body = self.fixtures.load("list_images.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_plans_list(self, method, url, body, headers): - body = self.fixtures.load("list_sizes.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_server_list(self, method, url, body, headers): - body = self.fixtures.load("list_nodes.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_server_list_SERVICE_UNAVAILABLE(self, method, url, body, headers): - body = self.fixtures.load("error_rate_limit.txt") - return ( - httplib.SERVICE_UNAVAILABLE, - body, - {}, - httplib.responses[httplib.SERVICE_UNAVAILABLE], - ) - - # pylint: disable=unused-argument - def _v1_server_create(self, method, url, body, headers): - body = self.fixtures.load("create_node.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_server_destroy(self, method, url, body, headers): - return (httplib.OK, "", {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_server_reboot(self, method, url, body, headers): - return (httplib.OK, "", {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_sshkey_list(self, method, url, body, headers): - body = self.fixtures.load("list_key_pairs.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_sshkey_create(self, method, url, body, headers): - body = self.fixtures.load("create_key_pair.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - # pylint: disable=unused-argument - def _v1_sshkey_destroy(self, method, url, body, headers): - return (httplib.OK, "", {}, httplib.responses[httplib.OK]) - - -if __name__ == "__main__": - sys.exit(unittest.main()) diff --git a/libcloud/test/dns/fixtures/vultr/delete_zone.json b/libcloud/test/dns/fixtures/vultr/delete_zone.json deleted file mode 100644 index fe51488c70..0000000000 --- a/libcloud/test/dns/fixtures/vultr/delete_zone.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/libcloud/test/dns/fixtures/vultr/empty_records_list.json b/libcloud/test/dns/fixtures/vultr/empty_records_list.json deleted file mode 100644 index fe51488c70..0000000000 --- a/libcloud/test/dns/fixtures/vultr/empty_records_list.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/libcloud/test/dns/fixtures/vultr/empty_zones_list.json b/libcloud/test/dns/fixtures/vultr/empty_zones_list.json deleted file mode 100644 index 41b42e677b..0000000000 --- a/libcloud/test/dns/fixtures/vultr/empty_zones_list.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - -] diff --git a/libcloud/test/dns/fixtures/vultr/get_record.json b/libcloud/test/dns/fixtures/vultr/get_record.json deleted file mode 100644 index 2c7145145a..0000000000 --- a/libcloud/test/dns/fixtures/vultr/get_record.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "type":"A", - "name":"zupo", - "data":"127.0.0.1", - "priority":0, - "RECORDID":1300 - - } - -] diff --git a/libcloud/test/dns/fixtures/vultr/get_zone.json b/libcloud/test/dns/fixtures/vultr/get_zone.json deleted file mode 100644 index 7639145980..0000000000 --- a/libcloud/test/dns/fixtures/vultr/get_zone.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "domain":"zupo.com", - "date_created":"2015-11-12 16:58:59" - } - -] diff --git a/libcloud/test/dns/fixtures/vultr/list_domains.json b/libcloud/test/dns/fixtures/vultr/list_domains.json deleted file mode 100644 index a2730cb2d1..0000000000 --- a/libcloud/test/dns/fixtures/vultr/list_domains.json +++ /dev/null @@ -1,21 +0,0 @@ -[ - { - "domain": "example.com", - "date_created": "2014-12-11 16:20:59" - }, - { - "domain": "zupo.com", - "date_created": "2014-12-11 16:21:50" - }, - - { - "domain":"oltjano.com", - "date_created": "2014-12-11 16:21:40" - }, - - { - "domain":"13.com", - "date_created":"2015-12-11 16:21:50" - } - -] diff --git a/libcloud/test/dns/fixtures/vultr/list_records.json b/libcloud/test/dns/fixtures/vultr/list_records.json deleted file mode 100644 index e6430831f1..0000000000 --- a/libcloud/test/dns/fixtures/vultr/list_records.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "type":"A", - "name":"arecord", - "data":"127.0.0.1", - "RECORDID":13 - }, - - { - "type":"CNAME", - "name":"*", - "data":"example.com", - "priority":0, - "RECORDID":1265277 - } - - - -] diff --git a/libcloud/test/dns/fixtures/vultr/test_zone.json b/libcloud/test/dns/fixtures/vultr/test_zone.json deleted file mode 100644 index 0cf5519bb7..0000000000 --- a/libcloud/test/dns/fixtures/vultr/test_zone.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - - { - "domain":"test.com", - "date_created":"1-1-2015 18:31:31" - - } - -] diff --git a/libcloud/test/dns/test_vultr.py b/libcloud/test/dns/test_vultr.py deleted file mode 100644 index a3409219b9..0000000000 --- a/libcloud/test/dns/test_vultr.py +++ /dev/null @@ -1,310 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. - -import sys -import unittest - -from libcloud.test import MockHttp -from libcloud.dns.base import Zone, Record -from libcloud.dns.types import ( - RecordType, - ZoneDoesNotExistError, - ZoneAlreadyExistsError, - RecordDoesNotExistError, -) -from libcloud.utils.py3 import httplib -from libcloud.test.secrets import VULTR_PARAMS -from libcloud.dns.drivers.vultr import VultrDNSDriver, VultrDNSDriverV1 -from libcloud.test.file_fixtures import DNSFileFixtures - - -class VultrTests(unittest.TestCase): - def setUp(self): - VultrMockHttp.type = None - VultrDNSDriverV1.connectionCls.conn_class = VultrMockHttp - self.driver = VultrDNSDriver(*VULTR_PARAMS, api_version="1") - self.test_zone = Zone( - id="test.com", - type="master", - ttl=None, - domain="test.com", - extra={}, - driver=self, - ) - self.test_record = Record( - id="31", - type=RecordType.A, - name="test", - zone=self.test_zone, - data="127.0.0.1", - driver=self, - extra={}, - ) - - def test_correct_class_is_used(self): - self.assertIsInstance(self.driver, VultrDNSDriverV1) - - def test_list_zones_empty(self): - VultrMockHttp.type = "EMPTY_ZONES_LIST" - zones = self.driver.list_zones() - - self.assertEqual(zones, []) - - def test_list_zones_success(self): - zones = self.driver.list_zones() - self.assertEqual(len(zones), 4) - - zone = zones[0] - self.assertEqual(zone.id, "example.com") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "example.com") - self.assertIsNone(zone.ttl) - - zone = zones[1] - self.assertEqual(zone.id, "zupo.com") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "zupo.com") - self.assertIsNone(zone.ttl) - - zone = zones[2] - self.assertEqual(zone.id, "oltjano.com") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "oltjano.com") - self.assertIsNone(zone.ttl) - - zone = zones[3] - self.assertEqual(zone.id, "13.com") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "13.com") - self.assertIsNone(zone.ttl) - - def test_get_zone_zone_does_not_exist(self): - VultrMockHttp.type = "GET_ZONE_ZONE_DOES_NOT_EXIST" - try: - self.driver.get_zone(zone_id="test.com") - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, "test.com") - else: - self.fail("Exception was not thrown") - - def test_get_zone_success(self): - VultrMockHttp.type = "GET_ZONE_SUCCESS" - zone = self.driver.get_zone(zone_id="zupo.com") - - self.assertEqual(zone.id, "zupo.com") - self.assertEqual(zone.domain, "zupo.com") - self.assertEqual(zone.type, "master") - self.assertIsNone(zone.ttl) - - def test_delete_zone_zone_does_not_exist(self): - VultrMockHttp.type = "DELETE_ZONE_ZONE_DOES_NOT_EXIST" - - try: - self.driver.delete_zone(zone=self.test_zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, self.test_zone.id) - else: - self.fail("Exception was not thrown") - - def test_delete_zone_success(self): - zone = self.driver.list_zones()[0] - status = self.driver.delete_zone(zone=zone) - - self.assertTrue(status) - - def test_create_zone_success(self): - zone = self.driver.create_zone(domain="test.com", extra={"serverip": "127.0.0.1"}) - - self.assertEqual(zone.id, "test.com") - self.assertEqual(zone.domain, "test.com") - self.assertEqual(zone.type, "master"), - self.assertIsNone(zone.ttl) - - def test_create_zone_zone_already_exists(self): - VultrMockHttp.type = "CREATE_ZONE_ZONE_ALREADY_EXISTS" - - try: - self.driver.create_zone(domain="example.com", extra={"serverip": "127.0.0.1"}) - except ZoneAlreadyExistsError as e: - self.assertEqual(e.zone_id, "example.com") - else: - self.fail("Exception was not thrown") - - def test_get_record_record_does_not_exist(self): - VultrMockHttp.type = "GET_RECORD_RECORD_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="zupo.com", record_id="1300") - except RecordDoesNotExistError as e: - self.assertEqual(e.record_id, "1300") - else: - self.fail("Exception was not thrown") - - def test_list_records_zone_does_not_exist(self): - VultrMockHttp.type = "LIST_RECORDS_ZONE_DOES_NOT_EXIST" - - try: - self.driver.list_records(zone=self.test_zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, self.test_zone.id) - else: - self.fail("Exception was not thrown") - - def test_list_records_empty(self): - VultrMockHttp.type = "EMPTY_RECORDS_LIST" - zone = self.driver.list_zones()[0] - records = self.driver.list_records(zone=zone) - - self.assertEqual(records, []) - - def test_list_records_success(self): - zone = self.driver.get_zone(zone_id="zupo.com") - records = self.driver.list_records(zone=zone) - self.assertEqual(len(records), 2) - - arecord = records[0] - self.assertEqual(arecord.id, "13") - self.assertEqual(arecord.name, "arecord") - self.assertEqual(arecord.type, RecordType.A) - self.assertEqual(arecord.data, "127.0.0.1") - - def test_get_record_success(self): - VultrMockHttp.type = "GET_RECORD" - record = self.driver.get_record(zone_id="zupo.com", record_id="1300") - - self.assertEqual(record.id, "1300") - self.assertEqual(record.name, "zupo") - self.assertEqual(record.data, "127.0.0.1") - self.assertEqual(record.type, RecordType.A) - - def test_delete_record_record_does_not_exist(self): - VultrMockHttp.type = "DELETE_RECORD_RECORD_DOES_NOT_EXIST" - - try: - self.driver.delete_record(record=self.test_record) - except RecordDoesNotExistError as e: - self.assertEqual(e.record_id, self.test_record.id) - else: - self.fail("Exception was not thrown") - - def test_delete_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - status = self.driver.delete_record(record=record) - - self.assertTrue(status) - - -class VultrMockHttp(MockHttp): - fixtures = DNSFileFixtures("vultr") - - def _v1_dns_list(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_records(self, method, url, body, headers): - body = self.fixtures.load("list_records.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_EMPTY_ZONES_LIST(self, method, url, body, headers): - body = self.fixtures.load("empty_zones_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_GET_ZONE_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_GET_ZONE_SUCCESS(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_EMPTY_RECORDS_LIST(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_records_EMPTY_RECORDS_LIST(self, method, url, body, headers): - body = self.fixtures.load("empty_records_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_GET_RECORD(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_records_GET_RECORD(self, method, url, body, headers): - body = self.fixtures.load("get_record.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_GET_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_records_GET_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_records.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_delete_domain(self, method, url, body, headers): - body = "" - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_delete_record(self, method, url, body, headers): - body = "" - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_create_domain(self, method, url, body, headers): - body = "" - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_CREATE_ZONE_ZONE_ALREADY_EXISTS(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_create_domain_CREATE_ZONE_ZONE_ALREADY_EXISTS(self, method, url, body, headers): - body = "" - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_DELETE_ZONE_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_delete_domain_DELETE_ZONE_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_records_DELETE_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_records.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_delete_record_DELETE_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_DELETE_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("test_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_list_LIST_RECORDS_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_domains.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _v1_dns_records_LIST_RECORDS_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): - body = "" - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - -if __name__ == "__main__": - sys.exit(unittest.main()) From 78dd1d641e155ee2238f1503aeea009201014221 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 12:14:34 -0500 Subject: [PATCH 39/52] dns: vultr: Add API request checking --- libcloud/test/dns/test_vultr_v2.py | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/libcloud/test/dns/test_vultr_v2.py b/libcloud/test/dns/test_vultr_v2.py index baa804509c..9792410bd7 100644 --- a/libcloud/test/dns/test_vultr_v2.py +++ b/libcloud/test/dns/test_vultr_v2.py @@ -25,6 +25,7 @@ class VultrTests(unittest.TestCase): def setUp(self): VultrMockHttp.type = None + VultrMockHttp.history.clear() VultrDNSDriverV2.connectionCls.conn_class = VultrMockHttp self.driver = VultrDNSDriver("foo") @@ -36,6 +37,11 @@ def test_unknown_api_version(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains") + self.assertEqual(len(zones), 2) zone = zones[0] self.assertEqual(zone.id, "example.com") @@ -44,12 +50,23 @@ def test_list_zones(self): def test_create_zone(self): zone = self.driver.create_zone("example.com") + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v2/domains") + self.assertEqual(sent.json["domain"], "example.com") + self.assertEqual(zone.id, "example.com") self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.extra["date_created"], "2021-09-07T10:28:34+00:00") def test_get_zone(self): zone = self.driver.get_zone("example.com") + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains/example.com") + self.assertEqual(zone.id, "example.com") self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.extra["date_created"], "2021-09-07T09:52:18+00:00") @@ -57,11 +74,21 @@ def test_get_zone(self): def test_delete_zone(self): zone = self.driver.get_zone("example.com") response = self.driver.delete_zone(zone) + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v2/domains/example.com") + self.assertTrue(response) def test_list_records(self): zone = self.driver.list_zones()[0] records = self.driver.list_records(zone) + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains/example.com/records") + self.assertEqual(len(records), 5) record = records[0] self.assertEqual(record.id, "123") @@ -72,6 +99,14 @@ def test_list_records(self): def test_create_record(self): zone = self.driver.list_zones()[0] record = self.driver.create_record("test1", zone, "A", "192.168.0.11") + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/v2/domains/example.com/records") + self.assertEqual(sent.json["name"], "test1") + self.assertEqual(sent.json["type"], "A") + self.assertEqual(sent.json["data"], "192.168.0.11") + self.assertEqual(record.id, "123") self.assertEqual(record.zone.domain, zone.domain) self.assertEqual(record.type, "A") @@ -84,12 +119,26 @@ def test_update_record(self): response = self.driver.update_record( record, name="test", data="192.168.0.0", extra=dict(ttl=300, priority=1) ) + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, "/v2/domains/example.com/records/123") + self.assertEqual(sent.json["name"], "test") + self.assertEqual(sent.json["data"], "192.168.0.0") + self.assertEqual(sent.json["ttl"], 300) + self.assertEqual(sent.json["priority"], 1) + self.assertTrue(response) def test_get_record(self): zone = self.driver.list_zones()[0] temp = self.driver.list_records(zone)[0] record = self.driver.get_record(zone.domain, temp.id) + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/v2/domains/example.com/records/123") + self.assertEqual(record.id, "123") self.assertEqual(record.zone.domain, zone.domain) self.assertEqual(record.type, "NS") @@ -101,11 +150,17 @@ def test_delete_record(self): zone = self.driver.list_zones()[0] record = self.driver.list_records(zone)[0] response = self.driver.delete_record(record) + + sent = VultrMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/v2/domains/example.com/records/123") + self.assertTrue(response) class VultrMockHttp(MockHttp): fixtures = DNSFileFixtures("vultr_v2") + keep_history = True def _v2_domains(self, method, url, body, headers): if method == "GET": From b1fba06d4d3b1bd1b51a2d6cd707db1258340327 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 12:41:19 -0500 Subject: [PATCH 40/52] dns: powerdns: Add API request checking --- libcloud/test/dns/test_powerdns.py | 53 +++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/libcloud/test/dns/test_powerdns.py b/libcloud/test/dns/test_powerdns.py index 340cc2804d..7370c715c6 100644 --- a/libcloud/test/dns/test_powerdns.py +++ b/libcloud/test/dns/test_powerdns.py @@ -28,6 +28,7 @@ class PowerDNSTestCase(LibcloudTestCase): def setUp(self): PowerDNSDriver.connectionCls.conn_class = PowerDNSMockHttp PowerDNSMockHttp.type = None + PowerDNSMockHttp.history.clear() self.driver = PowerDNSDriver("testsecret") self.test_zone = Zone( @@ -40,7 +41,7 @@ def setUp(self): ) self.test_record = Record( id=None, - name="", + name="example.com", data="192.0.2.1", type=RecordType.A, zone=self.test_zone, @@ -55,6 +56,17 @@ def test_create_record(self): data="192.0.5.4", extra={"ttl": 86400}, ) + + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, "/servers/localhost/zones/example.com.") + rrset = sent.json["rrsets"] + self.assertEqual(len(rrset), 1) + self.assertEqual(rrset[0]["name"], "newrecord.example.com") + self.assertEqual(rrset[0]["type"], "A") + self.assertEqual(rrset[0]["records"][0]["content"], "192.0.5.4") + self.assertEqual(rrset[0]["changetype"], "REPLACE") + self.assertIsNone(record.id) self.assertEqual(record.name, "newrecord.example.com") self.assertEqual(record.data, "192.0.5.4") @@ -64,6 +76,12 @@ def test_create_record(self): def test_create_zone(self): extra = {"nameservers": ["ns1.example.org", "ns2.example.org"]} zone = self.driver.create_zone("example.org", extra=extra) + + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/servers/localhost/zones") + self.assertEqual(sent.json["name"], "example.org") + self.assertEqual(zone.id, "example.org.") self.assertEqual(zone.domain, "example.org") self.assertIsNone(zone.type) @@ -75,12 +93,21 @@ def test_delete_record(self): def test_delete_zone(self): self.assertTrue(self.test_zone.delete()) + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/servers/localhost/zones/example.com.") + def test_get_record(self): with self.assertRaises(NotImplementedError): self.driver.get_record("example.com.", "12345") def test_get_zone(self): zone = self.driver.get_zone("example.com.") + + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/servers/localhost/zones/example.com.") + self.assertEqual(zone.id, "example.com.") self.assertEqual(zone.domain, "example.com") self.assertIsNone(zone.type) @@ -92,10 +119,20 @@ def test_list_record_types(self): def test_list_records(self): records = self.driver.list_records(self.test_zone) + + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/servers/localhost/zones/example.com.") + self.assertEqual(len(records), 4) def test_list_zones(self): zones = self.driver.list_zones() + + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/servers/localhost/zones") + self.assertEqual(zones[0].id, "example.com.") self.assertEqual(zones[0].domain, "example.com") self.assertIsNone(zones[0].type) @@ -113,6 +150,19 @@ def test_update_record(self): data="127.0.0.1", extra={"ttl": 300}, ) + + sent = PowerDNSMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, "/servers/localhost/zones/example.com.") + rrset = sent.json["rrsets"] + self.assertEqual(len(rrset), 2) + self.assertEqual(rrset[0]["name"], "example.com") + self.assertEqual(rrset[0]["changetype"], "DELETE") + self.assertEqual(rrset[1]["name"], "newrecord.example.com") + self.assertEqual(rrset[1]["type"], "A") + self.assertEqual(rrset[1]["records"][0]["content"], "127.0.0.1") + self.assertEqual(rrset[1]["changetype"], "REPLACE") + self.assertIsNone(record.id) self.assertEqual(record.name, "newrecord.example.com") self.assertEqual(record.data, "127.0.0.1") @@ -147,6 +197,7 @@ def test_delete_missing_zone(self): class PowerDNSMockHttp(MockHttp): fixtures = DNSFileFixtures("powerdns") + keep_history = True base_headers = {"content-type": "application/json"} def _servers_localhost_zones(self, method, url, body, headers): From 41fb5051fdc2b0d18a83c956e81656611e634f4b Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 13:08:34 -0500 Subject: [PATCH 41/52] dns: nfsn: Add API request checking --- libcloud/test/dns/test_nfsn.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/libcloud/test/dns/test_nfsn.py b/libcloud/test/dns/test_nfsn.py index 77b3e7e66a..5616026e1e 100644 --- a/libcloud/test/dns/test_nfsn.py +++ b/libcloud/test/dns/test_nfsn.py @@ -18,7 +18,7 @@ from libcloud.test import MockHttp, LibcloudTestCase from libcloud.dns.base import Zone, Record from libcloud.dns.types import RecordType, ZoneDoesNotExistError, RecordDoesNotExistError -from libcloud.utils.py3 import httplib +from libcloud.utils.py3 import httplib, urlparse from libcloud.dns.drivers.nfsn import NFSNDNSDriver from libcloud.test.file_fixtures import DNSFileFixtures @@ -27,6 +27,7 @@ class NFSNTestCase(LibcloudTestCase): def setUp(self): NFSNDNSDriver.connectionCls.conn_class = NFSNMockHttp NFSNMockHttp.type = None + NFSNMockHttp.history.clear() self.driver = NFSNDNSDriver("testid", "testsecret") self.test_zone = Zone( @@ -57,6 +58,11 @@ def test_create_zone(self): def test_get_zone(self): zone = self.driver.get_zone("example.com") + + sent = NFSNMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/dns/example.com/serial") + self.assertEqual(zone.id, None) self.assertEqual(zone.domain, "example.com") @@ -69,6 +75,18 @@ def test_create_record(self): record = self.test_zone.create_record( name="newrecord", type=RecordType.A, data="127.0.0.1", extra={"ttl": 900} ) + + # [0] /dns/example.com/addRR + # [1] /dns/example.com/listRRs + sent = NFSNMockHttp.history.pop(0) + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/dns/example.com/addRR") + query = urlparse.parse_qs(sent.body) + self.assertIn("newrecord", query["name"]) + self.assertIn("A", query["type"]) + self.assertIn("127.0.0.1", query["data"]) + self.assertIn("900", query["ttl"]) + self.assertEqual(record.id, None) self.assertEqual(record.name, "newrecord") self.assertEqual(record.data, "127.0.0.1") @@ -82,8 +100,20 @@ def test_get_record(self): def test_delete_record(self): self.assertTrue(self.test_record.delete()) + sent = NFSNMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/dns/example.com/removeRR") + query = urlparse.parse_qs(sent.body) + self.assertIn("A", query["type"]) + self.assertIn("192.0.2.1", query["data"]) + def test_list_records(self): records = self.driver.list_records(self.test_zone) + + sent = NFSNMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/dns/example.com/listRRs") + self.assertEqual(len(records), 2) def test_ex_get_records_by(self): @@ -109,6 +139,7 @@ def test_delete_record_not_found(self): class NFSNMockHttp(MockHttp): fixtures = DNSFileFixtures("nfsn") + keep_history = True base_headers = {"content-type": "application/x-nfsn-api"} def _dns_example_com_addRR_CREATED(self, method, url, body, headers): From b9a323d71c82d19128fa2f73300617a5f0d3f8e8 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 13:33:21 -0500 Subject: [PATCH 42/52] dns: gandi_live: Add API request checking --- libcloud/test/dns/test_gandi_live.py | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/libcloud/test/dns/test_gandi_live.py b/libcloud/test/dns/test_gandi_live.py index cab1176efc..2f7a66ca9c 100644 --- a/libcloud/test/dns/test_gandi_live.py +++ b/libcloud/test/dns/test_gandi_live.py @@ -38,6 +38,7 @@ class GandiLiveTests(unittest.TestCase): def setUp(self): GandiLiveDNSDriver.connectionCls.conn_class = GandiLiveMockHttp GandiLiveMockHttp.type = None + GandiLiveMockHttp.history.clear() self.driver = GandiLiveDNSDriver(*DNS_GANDI_LIVE) self.test_zone = Zone( id="example.com", @@ -76,6 +77,11 @@ def setUp(self): def test_list_zones(self): zones = self.driver.list_zones() + + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v5/domains") + self.assertEqual(len(zones), 2) zone = zones[0] self.assertEqual(zone.id, "example.com") @@ -90,18 +96,47 @@ def test_list_zones(self): def test_create_zone(self): zone = self.driver.create_zone("example.org", extra={"name": "Example"}) + + # [0] /api/v5/domains (create) + # [1] /api/v5/domains/example.org (modify) + sent = GandiLiveMockHttp.history.pop(0) + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/api/v5/zones") + self.assertEqual(sent.json["name"], "Example") + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, "/api/v5/domains/example.org") + self.assertEqual(sent.json["zone_uuid"], "54321") + self.assertEqual(zone.id, "example.org") self.assertEqual(zone.domain, "example.org") self.assertEqual(zone.extra["zone_uuid"], "54321") def test_create_zone_without_name(self): zone = self.driver.create_zone("example.org") + + # [0] /api/v5/domains (create) + # [1] /api/v5/domains/example.org (modify) + sent = GandiLiveMockHttp.history.pop(0) + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/api/v5/zones") + self.assertEqual(sent.json["name"], "example.org zone") + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "PATCH") + self.assertEqual(sent.url, "/api/v5/domains/example.org") + self.assertEqual(sent.json["zone_uuid"], "54321") + self.assertEqual(zone.id, "example.org") self.assertEqual(zone.domain, "example.org") self.assertEqual(zone.extra["zone_uuid"], "54321") def test_get_zone(self): zone = self.driver.get_zone("example.com") + + sent = GandiLiveMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v5/domains/example.com") + self.assertEqual(zone.id, "example.com") self.assertEqual(zone.type, "master") self.assertEqual(zone.domain, "example.com") @@ -109,6 +144,11 @@ def test_get_zone(self): def test_list_records(self): records = self.driver.list_records(self.test_zone) + + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v5/domains/example.com/records") + self.assertEqual(len(records), 3) record = records[0] self.assertEqual(record.id, "A:@") @@ -128,6 +168,13 @@ def test_list_records(self): def test_get_record(self): record = self.driver.get_record(self.test_zone.id, "A:bob") + + # [0] /api/v5/domains/example.com/records/bob/A + # [1] /api/v5/domains/example.com + sent = GandiLiveMockHttp.history.pop(0) + self.assertEqual(sent.method, "GET") + self.assertEqual(sent.url, "/api/v5/domains/example.com/records/bob/A") + self.assertEqual(record.id, "A:bob") self.assertEqual(record.name, "bob") self.assertEqual(record.type, RecordType.A) @@ -137,6 +184,15 @@ def test_create_record(self): record = self.driver.create_record( "alice", self.test_zone, "AAAA", "::1", extra={"ttl": 400} ) + + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "POST") + self.assertEqual(sent.url, "/api/v5/domains/example.com/records") + self.assertEqual(sent.json["rrset_name"], "alice") + self.assertEqual(sent.json["rrset_type"], "AAAA") + self.assertIn("::1", sent.json["rrset_values"]) + self.assertEqual(sent.json["rrset_ttl"], 400) + self.assertEqual(record.id, "AAAA:alice") self.assertEqual(record.name, "alice") self.assertEqual(record.type, RecordType.AAAA) @@ -168,6 +224,13 @@ def test_update_record(self): record = self.driver.update_record( self.test_record, "bob", RecordType.A, "192.168.0.2", {"ttl": 500} ) + + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "PUT") + self.assertEqual(sent.url, "/api/v5/domains/example.com/records/bob/A") + self.assertIn("192.168.0.2", sent.json["rrset_values"]) + self.assertEqual(sent.json["rrset_ttl"], 500) + self.assertEqual(record.id, "A:bob") self.assertEqual(record.name, "bob") self.assertEqual(record.type, RecordType.A) @@ -175,6 +238,11 @@ def test_update_record(self): def test_delete_record(self): success = self.driver.delete_record(self.test_record) + + sent = GandiLiveMockHttp.history.pop() + self.assertEqual(sent.method, "DELETE") + self.assertEqual(sent.url, "/api/v5/domains/example.com/records/bob/A") + self.assertTrue(success) def test_export_bind(self): @@ -259,6 +327,7 @@ def test_ex_delete_gandi_zone(self): class GandiLiveMockHttp(BaseGandiLiveMockHttp): fixtures = DNSFileFixtures("gandi_live") + keep_history = True def _json_api_v5_domains_get(self, method, url, body, headers): body = self.fixtures.load("list_zones.json") From a4edc490218f9c7fd7c0fe929f19b8bcf3b83e6f Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 15:17:21 -0500 Subject: [PATCH 43/52] dns: zonomi: Switch to default local ID format Previously, the Zonomi DNS driver could only return the first record for a given host, because it used the name of the host as Record.id. Now, use the new default, [:], format to allow various records to be differentiated for a given host. --- libcloud/dns/drivers/zonomi.py | 8 +++++--- libcloud/test/dns/test_zonomi.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/libcloud/dns/drivers/zonomi.py b/libcloud/dns/drivers/zonomi.py index da570466ca..b92b8a17e5 100644 --- a/libcloud/dns/drivers/zonomi.py +++ b/libcloud/dns/drivers/zonomi.py @@ -118,9 +118,10 @@ def get_record(self, zone_id, record_id): record = None zone = self.get_zone(zone_id=zone_id) records = self.list_records(zone=zone) + parts = self.from_default_id(zone, record_id) for r in records: - if r.id == record_id: + if r.name == parts.name and r.type == parts.type: record = r if record is None: @@ -320,11 +321,12 @@ def _to_record(self, item, zone): record_name = full_domain[:index] else: record_name = zone.domain + rtype = item["type"] record = Record( - id=record_name, + id=self.to_default_id(zone, item["name"], rtype), name=record_name, data=item["content"], - type=item["type"], + type=rtype, zone=zone, driver=self, ttl=ttl, diff --git a/libcloud/test/dns/test_zonomi.py b/libcloud/test/dns/test_zonomi.py index dfc9a6065b..1357dea00a 100644 --- a/libcloud/test/dns/test_zonomi.py +++ b/libcloud/test/dns/test_zonomi.py @@ -45,7 +45,7 @@ def setUp(self): extra={}, ) self.test_record = Record( - id="record.zone.com", + id="A:record", name="record.zone.com", data="127.0.0.1", type="A", @@ -184,28 +184,28 @@ def test_list_records_success(self): self.assertEqual(len(records), 4) record = records[0] - self.assertEqual(record.id, "zone.com") + self.assertEqual(record.id, "SOA") self.assertEqual(record.type, "SOA") self.assertEqual(record.data, "ns1.zonomi.com. soacontact.zonomi.com. 13") self.assertEqual(record.name, "zone.com") self.assertEqual(record.zone, self.test_zone) second_record = records[1] - self.assertEqual(second_record.id, "zone.com") + self.assertEqual(second_record.id, "NS") self.assertEqual(second_record.name, "zone.com") self.assertEqual(second_record.type, "NS") self.assertEqual(second_record.data, "ns1.zonomi.com") self.assertEqual(second_record.zone, self.test_zone) third_record = records[2] - self.assertEqual(third_record.id, "oltjano") + self.assertEqual(third_record.id, "A:oltjano") self.assertEqual(third_record.name, "oltjano") self.assertEqual(third_record.type, "A") self.assertEqual(third_record.data, "127.0.0.1") self.assertEqual(third_record.zone, self.test_zone) fourth_record = records[3] - self.assertEqual(fourth_record.id, "zone.com") + self.assertEqual(fourth_record.id, "NS") self.assertEqual(fourth_record.name, "zone.com") self.assertEqual(fourth_record.type, "NS") self.assertEqual(fourth_record.data, "ns5.zonomi.com") @@ -239,7 +239,7 @@ def test_get_record_success(self): driver=self.driver, ) self.driver.get_zone = MagicMock(return_value=zone) - record = self.driver.get_record(record_id="oltjano", zone_id="zone.com") + record = self.driver.get_record(record_id="A:oltjano", zone_id="zone.com") sent = ZonomiMockHttp.history.pop() self.assertEqual(sent.method, "GET") @@ -247,7 +247,7 @@ def test_get_record_success(self): self.assertIn("QUERY", sent.query["action"]) self.assertIn("**.zone.com", sent.query["name"]) - self.assertEqual(record.id, "oltjano") + self.assertEqual(record.id, "A:oltjano") self.assertEqual(record.name, "oltjano") self.assertEqual(record.type, "A") self.assertEqual(record.data, "127.0.0.1") @@ -303,7 +303,7 @@ def test_create_record_success(self): self.assertIn("A", sent.query["type"]) self.assertIn("127.0.0.1", sent.query["value"]) - self.assertEqual(record.id, "createrecord") + self.assertEqual(record.id, "A:createrecord") self.assertEqual(record.name, "createrecord") self.assertEqual(record.type, "A") self.assertEqual(record.data, "127.0.0.1") From 1a988f005a65b9915bb3371bc5563f0f701f0ff8 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 15:47:25 -0500 Subject: [PATCH 44/52] dns: pointdns: Make records agree with zone --- libcloud/test/dns/fixtures/pointdns/_zones_1_records_GET.json | 2 +- libcloud/test/dns/test_pointdns.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libcloud/test/dns/fixtures/pointdns/_zones_1_records_GET.json b/libcloud/test/dns/fixtures/pointdns/_zones_1_records_GET.json index 607a68d047..a2a1468b79 100644 --- a/libcloud/test/dns/fixtures/pointdns/_zones_1_records_GET.json +++ b/libcloud/test/dns/fixtures/pointdns/_zones_1_records_GET.json @@ -11,7 +11,7 @@ }, { "zone_record": { - "name": "site.example1.com", + "name": "site1.example.com", "data": "1.2.3.6", "id": 150, "aux": null, diff --git a/libcloud/test/dns/test_pointdns.py b/libcloud/test/dns/test_pointdns.py index 7e94dc1985..de177a6780 100644 --- a/libcloud/test/dns/test_pointdns.py +++ b/libcloud/test/dns/test_pointdns.py @@ -92,7 +92,7 @@ def test_list_records_success(self): record2 = records[1] self.assertEqual(record2.id, "150") - self.assertEqual(record2.name, "site.example1.com") + self.assertEqual(record2.name, "site1.example.com") self.assertEqual(record2.type, RecordType.A) self.assertEqual(record2.data, "1.2.3.6") self.assertHasKeys(record2.extra, ["ttl", "zone_id", "aux"]) From aa3d0816e21c945d0cd1b2ef5e0d2a09bdbc4ceb Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 16:39:35 -0500 Subject: [PATCH 45/52] dns: luadns: Make records agree with zones --- libcloud/test/dns/fixtures/luadns/create_record_success.json | 2 +- libcloud/test/dns/fixtures/luadns/get_record.json | 2 +- libcloud/test/dns/fixtures/luadns/records_list.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libcloud/test/dns/fixtures/luadns/create_record_success.json b/libcloud/test/dns/fixtures/luadns/create_record_success.json index c72c2b9708..6e79aaa3ec 100644 --- a/libcloud/test/dns/fixtures/luadns/create_record_success.json +++ b/libcloud/test/dns/fixtures/luadns/create_record_success.json @@ -1,6 +1,6 @@ { "id": 31, - "name": "test.com.", + "name": "example.com.", "type": "A", "content": "127.0.0.1", "ttl": 13, diff --git a/libcloud/test/dns/fixtures/luadns/get_record.json b/libcloud/test/dns/fixtures/luadns/get_record.json index 29dc374e15..c42441e606 100644 --- a/libcloud/test/dns/fixtures/luadns/get_record.json +++ b/libcloud/test/dns/fixtures/luadns/get_record.json @@ -1,6 +1,6 @@ { "id": 31, - "name": "example.com.", + "name": "example.org.", "type": "MX", "content": "10 mail.example.com.", "ttl": 300, diff --git a/libcloud/test/dns/fixtures/luadns/records_list.json b/libcloud/test/dns/fixtures/luadns/records_list.json index d4cd30a686..826073394e 100644 --- a/libcloud/test/dns/fixtures/luadns/records_list.json +++ b/libcloud/test/dns/fixtures/luadns/records_list.json @@ -1,7 +1,7 @@ [ { "id": 6683, - "name": "example.org.", + "name": "example.com.", "type": "NS", "content": "b.ns.luadns.net.", "ttl": 86400, @@ -11,7 +11,7 @@ }, { "id": 6684, - "name": "example.org.", + "name": "example.com.", "type": "NS", "content": "a.ns.luadns.net.", "ttl": 86400, From f5e39a53ef7dc93a50a93f5094b974df39c28920 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 17:36:08 -0500 Subject: [PATCH 46/52] dns: Add common code for domain/host naming DNS drivers share the need to manipulate host/domain names, so let's add utility functions to the common Record and Zone classes to allow redundant code scattered throughout the various drivers to be consolidated into common and consistent implementations. * Record.name - no longer driver-dependent, now the host prefix or "" * Record.hostname - the complete host name, including domain part * Record.fqdn - the fully qualified domain name, including root dot * Zone.domain - no longer driver-dependent, now the unrooted domain * Zone.rooted() - format a (un)rooted domain as a rooted domain * Zone.rooted() - format a (un)rooted domain as an unrooted domain --- libcloud/dns/base.py | 37 ++++++++++++++++++++++++++-- libcloud/dns/drivers/durabledns.py | 10 +++++--- libcloud/dns/drivers/google.py | 5 ++-- libcloud/dns/drivers/luadns.py | 2 +- libcloud/dns/drivers/nsone.py | 2 +- libcloud/dns/drivers/pointdns.py | 4 +-- libcloud/dns/drivers/powerdns.py | 16 ++++++------ libcloud/dns/drivers/zonomi.py | 18 ++++---------- libcloud/test/dns/test_base.py | 26 ++++++++++++++++++- libcloud/test/dns/test_cloudflare.py | 6 ++--- libcloud/test/dns/test_durabledns.py | 10 ++++---- libcloud/test/dns/test_google.py | 10 ++++---- libcloud/test/dns/test_liquidweb.py | 2 +- libcloud/test/dns/test_luadns.py | 12 ++++----- libcloud/test/dns/test_nsone.py | 6 ++--- libcloud/test/dns/test_pointdns.py | 12 ++++----- libcloud/test/dns/test_powerdns.py | 4 +-- libcloud/test/dns/test_zonomi.py | 6 ++--- 18 files changed, 121 insertions(+), 67 deletions(-) diff --git a/libcloud/dns/base.py b/libcloud/dns/base.py index 385aeff039..245acbd1c0 100644 --- a/libcloud/dns/base.py +++ b/libcloud/dns/base.py @@ -60,7 +60,7 @@ def __init__( :type extra: ``dict`` """ self.id = str(id) if id else None - self.domain = domain + self.domain = self.unrooted(domain) self.type = type self.ttl = ttl or None self.driver = driver @@ -104,6 +104,16 @@ def __repr__(self): self.driver.name, ) + @staticmethod + def unrooted(domain): + """Return the provided domain as an unrooted domain""" + return domain.rstrip(".") + + @staticmethod + def rooted(domain): + """Return the provided domain as an unrooted domain""" + return domain if domain.endswith(".") else f"{domain}." + def prefix(self, subname): """ Accept subordinate (or identity) names in multiple convenience formats. @@ -211,7 +221,17 @@ def __init__( :type extra: ``dict`` """ self.id = str(id) if id else None - self.name = name + + # Support callers that have: + # 1. used None, rather than the empty string to indicate apex/naked + # records, while consistently setting the empty string. + # 2. used the full hostname (i.e. including domain), rather than the + # bare prefix (i.e. excluding domain) + # 3. used the fully qualified domain name (i.e. including domain and + # trailing dot), rather than the bare prefix. + # + self.name = "" if name is None else zone.prefix(name) + self.type = type self.data = data self.zone = zone @@ -252,6 +272,19 @@ def _get_numeric_id(self): return record_id + @property + def hostname(self): + """Return the complete hostname, including domain, for this record.""" + return self.zone.hostname(self.name) + + @property + def fqdn(self): + """ + Return the traditional fully qualified domain name, including full-stop + trailing dot, for this record. + """ + return self.zone.fqdn(self.name) + def __repr__(self): # type: () -> str zone = self.zone.domain if self.zone.domain else self.zone.id diff --git a/libcloud/dns/drivers/durabledns.py b/libcloud/dns/drivers/durabledns.py index 23dfd7563c..9c4ef73a6f 100644 --- a/libcloud/dns/drivers/durabledns.py +++ b/libcloud/dns/drivers/durabledns.py @@ -293,7 +293,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None): params = { "apiuser": self.key, "apikey": self.secret, - "zonename": domain, + "zonename": Zone.rooted(domain), "ttl": ttl or DEFAULT_TTL, } params.update(extra) @@ -312,7 +312,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None): req_data = skel % ( self.key, self.secret, - domain, + Zone.rooted(domain), extra.get("ns"), extra.get("mbox"), extra.get("refresh"), @@ -366,6 +366,7 @@ def create_record(self, name, zone, type, data, extra=None): :rtype: :class:`Record` """ + name = zone.prefix(name) if extra is None: extra = RECORD_EXTRA_PARAMS_DEFAULT_VALUES else: @@ -476,7 +477,7 @@ def update_zone(self, zone, domain, type="master", ttl=None, extra=None): params = { "apiuser": self.key, "apikey": self.secret, - "zonename": domain, + "zonename": Zone.rooted(domain), "ttl": ttl, } params.update(extra) @@ -495,7 +496,7 @@ def update_zone(self, zone, domain, type="master", ttl=None, extra=None): req_data = skel % ( self.key, self.secret, - domain, + Zone.rooted(domain), extra["ns"], extra["mbox"], extra["refresh"], @@ -545,6 +546,7 @@ def update_record(self, record, name, type, data, extra=None): :rtype: :class:`Record` """ zone = record.zone + name = zone.prefix(name) if extra is None: extra = record.extra else: diff --git a/libcloud/dns/drivers/google.py b/libcloud/dns/drivers/google.py index 376186debc..c2732fb5f3 100644 --- a/libcloud/dns/drivers/google.py +++ b/libcloud/dns/drivers/google.py @@ -185,7 +185,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None): name = self._cleanup_domain(domain) data = { - "dnsName": domain, + "dnsName": Zone.rooted(domain), "name": name, "description": description, } @@ -216,6 +216,7 @@ def create_record(self, name, zone, type, data, extra=None): :rtype: :class:`Record` """ + name = zone.fqdn(name) ttl = data.get("ttl", 0) rrdatas = data.get("rrdatas", []) @@ -253,7 +254,7 @@ def delete_record(self, record): data = { "deletions": [ { - "name": record.name, + "name": record.fqdn, "type": record.type, "rrdatas": record.data["rrdatas"], "ttl": record.data["ttl"], diff --git a/libcloud/dns/drivers/luadns.py b/libcloud/dns/drivers/luadns.py index ccb1cdfb58..9d5f26e34b 100644 --- a/libcloud/dns/drivers/luadns.py +++ b/libcloud/dns/drivers/luadns.py @@ -233,7 +233,7 @@ def create_record(self, name, zone, type, data, extra=None): :rtype: :class:`Record` """ action = "/v1/zones/%s/records" % zone.id - to_post = {"name": name, "content": data, "type": type, "zone_id": int(zone.id)} + to_post = {"name": zone.fqdn(name), "content": data, "type": type, "zone_id": int(zone.id)} # ttl is required to create a record for luadns # pass it through extra like this: extra={'ttl':ttl} if extra is not None: diff --git a/libcloud/dns/drivers/nsone.py b/libcloud/dns/drivers/nsone.py index 9b59fc3cc1..28b03d3843 100644 --- a/libcloud/dns/drivers/nsone.py +++ b/libcloud/dns/drivers/nsone.py @@ -190,7 +190,7 @@ def delete_record(self, record): :return: Boolean """ - action = "/v1/zones/{}/{}/{}".format(record.zone.domain, record.name, record.type) + action = "/v1/zones/{}/{}/{}".format(record.zone.domain, record.hostname, record.type) try: response = self.connection.request(action=action, method="DELETE") except NsOneException as e: diff --git a/libcloud/dns/drivers/pointdns.py b/libcloud/dns/drivers/pointdns.py index 98dbfb8aa5..26d4d2d8b8 100644 --- a/libcloud/dns/drivers/pointdns.py +++ b/libcloud/dns/drivers/pointdns.py @@ -280,7 +280,7 @@ def create_record(self, name, zone, type, data, extra=None): :rtype: :class:`Record` """ - r_json = {"name": name, "data": data, "record_type": type} + r_json = {"name": zone.hostname(name), "data": data, "record_type": type} if extra is not None: r_json.update(extra) r_data = json.dumps({"zone_record": r_json}) @@ -354,7 +354,7 @@ def update_record(self, record, name, type, data, extra=None): :rtype: :class:`Record` """ zone = record.zone - r_json = {"name": name, "data": data, "record_type": type} + r_json = {"name": zone.hostname(name), "data": data, "record_type": type} if extra is not None: r_json.update(extra) r_data = json.dumps({"zone_record": r_json}) diff --git a/libcloud/dns/drivers/powerdns.py b/libcloud/dns/drivers/powerdns.py index fd9a2dc743..92101cec74 100644 --- a/libcloud/dns/drivers/powerdns.py +++ b/libcloud/dns/drivers/powerdns.py @@ -185,18 +185,19 @@ def create_record(self, name, zone, type, data, extra=None): if extra is None or extra.get("ttl", None) is None: raise ValueError("PowerDNS requires a ttl value for every record") + hostname = zone.hostname(name) if self._pdns_version() == 3: record = { "content": data, "disabled": False, - "name": name, + "name": hostname, "ttl": extra["ttl"], "type": type, } payload = { "rrsets": [ { - "name": name, + "name": hostname, "type": type, "changetype": "REPLACE", "records": [record], @@ -212,7 +213,7 @@ def create_record(self, name, zone, type, data, extra=None): payload = { "rrsets": [ { - "name": name, + "name": hostname, "type": type, "changetype": "REPLACE", "ttl": extra["ttl"], @@ -426,19 +427,20 @@ def update_record(self, record, name, type, data, extra=None): if extra is None or extra.get("ttl", None) is None: raise ValueError("PowerDNS requires a ttl value for every record") + hostname = record.zone.hostname(name) if self._pdns_version() == 3: updated_record = { "content": data, "disabled": False, - "name": name, + "name": hostname, "ttl": extra["ttl"], "type": type, } payload = { "rrsets": [ - {"name": record.name, "type": record.type, "changetype": "DELETE"}, + {"name": record.hostname, "type": record.type, "changetype": "DELETE"}, { - "name": name, + "name": hostname, "type": type, "changetype": "REPLACE", "records": [updated_record], @@ -457,7 +459,7 @@ def update_record(self, record, name, type, data, extra=None): payload = { "rrsets": [ { - "name": name, + "name": hostname, "type": type, "changetype": "REPLACE", "ttl": extra["ttl"], diff --git a/libcloud/dns/drivers/zonomi.py b/libcloud/dns/drivers/zonomi.py index b92b8a17e5..7b42500f9f 100644 --- a/libcloud/dns/drivers/zonomi.py +++ b/libcloud/dns/drivers/zonomi.py @@ -176,10 +176,7 @@ def create_record(self, name, zone, type, data, extra=None): :rtype: :class:`Record` """ action = "/app/dns/dyndns.jsp?" - if name: - record_name = name + "." + zone.domain - else: - record_name = zone.domain + record_name = zone.hostname(name) params = {"action": "SET", "name": record_name, "value": data, "type": type} if type == "MX" and extra is not None: @@ -237,7 +234,7 @@ def delete_record(self, record): :rtype: Bool """ action = "/app/dns/dyndns.jsp?" - params = {"action": "DELETE", "name": record.name, "type": record.type} + params = {"action": "DELETE", "name": record.hostname, "type": record.type} try: response = self.connection.request(action=action, params=params) except ZonomiException as e: @@ -315,16 +312,11 @@ def _to_record(self, item, zone): else: ttl = None extra = {"ttl": ttl, "prio": item.get("prio")} - if len(item["name"]) > len(zone.domain): - full_domain = item["name"] - index = full_domain.index("." + zone.domain) - record_name = full_domain[:index] - else: - record_name = zone.domain + rname = item["name"] rtype = item["type"] record = Record( - id=self.to_default_id(zone, item["name"], rtype), - name=record_name, + id=self.to_default_id(zone, rname, rtype), + name=rname, data=item["content"], type=rtype, zone=zone, diff --git a/libcloud/test/dns/test_base.py b/libcloud/test/dns/test_base.py index e6c1aeac95..717a4af48b 100644 --- a/libcloud/test/dns/test_base.py +++ b/libcloud/test/dns/test_base.py @@ -84,6 +84,30 @@ def test_zone_helpers(self): self.assertEqual(zone.hostname(sub), "sub.example.com") self.assertEqual(zone.fqdn(sub), "sub.example.com.") + def test_record_init(self): + common = { + "id": None, + "name": None, + "type": RecordType.A, + "data": "0.0.0.0", + "zone": self.master_zone, + "driver": self.master_zone, + } + + for apex in (None, "", "example.com", "example.com."): + common["name"] = apex + r1 = Record(**common) + self.assertEqual(r1.name, "") + self.assertEqual(r1.hostname, "example.com") + self.assertEqual(r1.fqdn, "example.com.") + + for sub in ("sub", "sub.example.com", "sub.example.com."): + common["name"] = sub + r2 = Record(**common) + self.assertEqual(r2.name, "sub") + self.assertEqual(r2.hostname, "sub.example.com") + self.assertEqual(r2.fqdn, "sub.example.com.") + def test_export_zone_to_bind_format_slave_should_throw(self): zone = Zone(id=1, domain="example.com", type="slave", ttl=900, driver=self.driver) self.assertRaises(ValueError, zone.export_to_bind_format) @@ -159,7 +183,7 @@ def test_export_zone_to_bind_format_success(self): def test_get_numeric_id(self): values = MOCK_RECORDS_VALUES[0].copy() values["driver"] = self.driver - values["zone"] = None + values["zone"] = self.master_zone record = Record(**values) record.id = "abcd" diff --git a/libcloud/test/dns/test_cloudflare.py b/libcloud/test/dns/test_cloudflare.py index cd1b2cb42f..acded3c693 100644 --- a/libcloud/test/dns/test_cloudflare.py +++ b/libcloud/test/dns/test_cloudflare.py @@ -80,7 +80,7 @@ def test_get_record(self): self.assertEqual(sent.url, "/client/v4/zones/1234/dns_records/364797364") self.assertEqual(record.id, "364797364") - self.assertIsNone(record.name) + self.assertEqual(record.name, "") self.assertEqual(record.type, "A") self.assertEqual(record.data, "192.30.252.153") @@ -104,7 +104,7 @@ def test_list_records(self): record = records[0] self.assertEqual(record.id, "364797364") - self.assertIsNone(record.name) + self.assertEqual(record.name, "") self.assertEqual(record.type, "A") self.assertEqual(record.data, "192.30.252.153") self.assertEqual(record.extra["priority"], None) @@ -123,7 +123,7 @@ def test_list_records(self): record = [r for r in records if r.type == "MX"][0] self.assertEqual(record.id, "78526") - self.assertIsNone(record.name) + self.assertEqual(record.name, "") self.assertEqual(record.type, "MX") self.assertEqual(record.data, "aspmx3.googlemail.com") self.assertEqual(record.extra["priority"], 30) diff --git a/libcloud/test/dns/test_durabledns.py b/libcloud/test/dns/test_durabledns.py index 1e77375c3d..dac7320899 100644 --- a/libcloud/test/dns/test_durabledns.py +++ b/libcloud/test/dns/test_durabledns.py @@ -90,7 +90,7 @@ def test_list_zones(self): self.assertEqual(len(zones), 2) zone = zones[0] self.assertEqual(zone.id, "myzone.com.") - self.assertEqual(zone.domain, "myzone.com.") + self.assertEqual(zone.domain, "myzone.com") self.assertEqual(zone.ttl, 1300) self.assertEqual(zone.extra["ns"], "ns1.durabledns.com.") self.assertEqual(zone.extra["mbox"], "mail.myzone.com") @@ -187,7 +187,7 @@ def test_get_zone(self): self.assertIn(":zonename>myzone.com.deletedzone.com. Date: Wed, 2 Apr 2025 18:17:44 -0500 Subject: [PATCH 47/52] dns: google: Leverage {to,from}_default_id() --- libcloud/dns/drivers/google.py | 13 +++++-------- libcloud/test/dns/test_google.py | 12 ++++++------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/libcloud/dns/drivers/google.py b/libcloud/dns/drivers/google.py index c2732fb5f3..3ecacab552 100644 --- a/libcloud/dns/drivers/google.py +++ b/libcloud/dns/drivers/google.py @@ -134,11 +134,12 @@ def get_record(self, zone_id, record_id): :rtype: :class:`Record` """ - (record_type, record_name) = record_id.split(":", 1) + zone = self.get_zone(zone_id) + parts = self.from_default_id(zone, record_id) params = { - "name": record_name, - "type": record_type, + "name": parts.name, + "type": parts.type, } request = "/managedZones/%s/rrsets" % (zone_id) @@ -149,8 +150,6 @@ def get_record(self, zone_id, record_id): raise ZoneDoesNotExistError(value="", driver=self.connection.driver, zone_id=zone_id) if len(response["rrsets"]) > 0: - zone = self.get_zone(zone_id) - return self._to_record(response["rrsets"][0], zone) raise RecordDoesNotExistError(value="", driver=self.connection.driver, record_id=record_id) @@ -386,10 +385,8 @@ def _to_records(self, response, zone): return records def _to_record(self, r, zone): - record_id = "{}:{}".format(r["type"], r["name"]) - return Record( - id=record_id, + id=self.to_default_id(zone, r["name"], r["type"]), name=r["name"], type=r["type"], data=r, diff --git a/libcloud/test/dns/test_google.py b/libcloud/test/dns/test_google.py index 415eed870f..62780f55c8 100644 --- a/libcloud/test/dns/test_google.py +++ b/libcloud/test/dns/test_google.py @@ -85,15 +85,15 @@ def test_get_record(self): zone = self.driver.list_zones()[0] GoogleDNSMockHttp.history.clear() - record = self.driver.get_record(zone.id, "A:foo.example.com.") + record = self.driver.get_record(zone.id, "A:foo") - # [0] /dns/v1/projects/project_name/managedZones/{zone.id}/rrsets - # [1] /dns/v1/projects/project_name/managedZones/{zone.id} - sent = GoogleDNSMockHttp.history.pop(0) + # [0] /dns/v1/projects/project_name/managedZones/{zone.id} + # [1] /dns/v1/projects/project_name/managedZones/{zone.id}/rrsets + sent = GoogleDNSMockHttp.history.pop() self.assertEqual(sent.method, "GET") self.assertEqual(sent.url, f"/dns/v1/projects/project_name/managedZones/{zone.id}/rrsets") - self.assertEqual(record.id, "A:foo.example.com.") + self.assertEqual(record.id, "A:foo") self.assertEqual(record.fqdn, "foo.example.com.") self.assertEqual(record.type, "A") self.assertEqual(record.zone.id, "example-com") @@ -102,7 +102,7 @@ def test_get_record_zone_does_not_exist(self): GoogleDNSMockHttp.type = "ZONE_DOES_NOT_EXIST" try: - self.driver.get_record("example-com", "a:a") + self.driver.get_record("example-com", "A:a") except ZoneDoesNotExistError as e: self.assertEqual(e.zone_id, "example-com") else: From 59e3125103efda8048230d0e0ae2d8fb4d8d2e67 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Wed, 2 Apr 2025 18:20:39 -0500 Subject: [PATCH 48/52] dns: google: Drop FQDN from inputs Update testing inputs to conform to spec'ed usage. --- libcloud/test/dns/test_google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libcloud/test/dns/test_google.py b/libcloud/test/dns/test_google.py index 62780f55c8..b60bcded83 100644 --- a/libcloud/test/dns/test_google.py +++ b/libcloud/test/dns/test_google.py @@ -119,7 +119,7 @@ def test_get_record_record_does_not_exist(self): def test_create_zone(self): extra = {"description": "new domain for example.org"} - zone = self.driver.create_zone("example.org.", extra) + zone = self.driver.create_zone("example.org", extra) sent = GoogleDNSMockHttp.history.pop() self.assertEqual(sent.method, "POST") From f21850b98fbf3ecf2b73adf735f16334adadf062 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 3 Apr 2025 10:00:59 -0500 Subject: [PATCH 49/52] dns: gandi: Leverage {to,from}_default_id() --- libcloud/dns/drivers/gandi.py | 9 +++++---- libcloud/test/dns/test_gandi.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/libcloud/dns/drivers/gandi.py b/libcloud/dns/drivers/gandi.py index 675bec0858..68fbd7b3b4 100644 --- a/libcloud/dns/drivers/gandi.py +++ b/libcloud/dns/drivers/gandi.py @@ -157,7 +157,7 @@ def _to_record(self, record, zone): extra["priority"] = int(split[0]) value = split[1] return Record( - id="{}:{}".format(record["type"], record["name"]), + id=self.to_default_id(zone, record["name"], record["type"]), name=record["name"], type=self._string_to_record_type(record["type"]), data=value, @@ -181,15 +181,16 @@ def list_records(self, zone): def get_record(self, zone_id, record_id): zid = int(zone_id) - record_type, name = record_id.split(":", 1) - filter_opts = {"name": name, "type": record_type} + zone = self.get_zone(zone_id) + rparts = self.from_default_id(zone, record_id) + filter_opts = {"name": rparts.name, "type": rparts.type} self.connection.set_context({"zone_id": zone_id}) records = self.connection.request("domain.zone.record.list", zid, 0, filter_opts).object if len(records) == 0: raise RecordDoesNotExistError(value="", driver=self, record_id=record_id) - return self._to_record(records[0], self.get_zone(zone_id)) + return self._to_record(records[0], zone) def _validate_record(self, record_id, name, record_type, data, extra): if len(data) > 1024: diff --git a/libcloud/test/dns/test_gandi.py b/libcloud/test/dns/test_gandi.py index 8e21c3b7f4..140daafb72 100644 --- a/libcloud/test/dns/test_gandi.py +++ b/libcloud/test/dns/test_gandi.py @@ -72,7 +72,7 @@ def test_list_records(self): record = records[3] self.assertEqual(record.name, "") - self.assertEqual(record.id, "MX:") + self.assertEqual(record.id, "MX") self.assertEqual(record.type, RecordType.MX) self.assertEqual(record.data, "aspmx.l.google.com") self.assertEqual(record.extra["priority"], 15) @@ -366,7 +366,7 @@ def _xmlrpc__domain_zone_record_list_RECORD_DOES_NOT_EXIST(self, method, url, bo return (httplib.OK, body, {}, httplib.responses[httplib.OK]) def _xmlrpc__domain_zone_info_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): - body = self.fixtures.load("list_zones.xml") + body = self.fixtures.load("get_zone.xml") return (httplib.OK, body, {}, httplib.responses[httplib.OK]) def _xmlrpc__domain_zone_record_delete_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): From 2ec65db13768cb71f67f8ee9562d6281c883bfd9 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 3 Apr 2025 11:14:31 -0500 Subject: [PATCH 50/52] dns: route53: Leverage common code Leverage common code from DNSDriver, Record, and Zone classes for host name and record ID handling. --- libcloud/dns/drivers/route53.py | 52 +++++++++---------------------- libcloud/test/dns/test_route53.py | 12 +++---- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/libcloud/dns/drivers/route53.py b/libcloud/dns/drivers/route53.py index 6140cc6cb6..79be93da2d 100644 --- a/libcloud/dns/drivers/route53.py +++ b/libcloud/dns/drivers/route53.py @@ -129,14 +129,11 @@ def get_zone(self, zone_id): def get_record(self, zone_id, record_id): zone = self.get_zone(zone_id=zone_id) - record_type, name = record_id.split(":", 1) - - if name: - full_name = ".".join((name, zone.domain)) - else: - full_name = zone.domain + rparts = self.from_default_id(zone, record_id) self.connection.set_context({"zone_id": zone_id}) - params = urlencode({"name": full_name, "type": record_type, "maxitems": "1"}) + params = urlencode( + {"name": zone.hostname(rparts.name), "type": rparts.type, "maxitems": "1"} + ) uri = API_ROOT + "hostedzone/" + zone_id + "/rrset?" + params data = self.connection.request(uri).object @@ -145,9 +142,9 @@ def get_record(self, zone_id, record_id): # A cute aspect of the /rrset filters is that they are more pagination # hints than filters!! # So will return a result even if its not what you asked for. - record_type_num = self._string_to_record_type(record_type) + record_type_num = self._string_to_record_type(rparts.type) - if record.name != name or record.type != record_type_num: + if record.name != rparts.name or record.type != record_type_num: raise RecordDoesNotExistError(value="", driver=self, record_id=record_id) return record @@ -186,10 +183,9 @@ def create_record(self, name, zone, type, data, extra=None): extra = extra or {} batch = [("CREATE", name, type, data, extra)] self._post_changeset(zone, batch) - id = ":".join((self.RECORD_TYPE_MAP[type], name)) return Record( - id=id, + id=self.to_default_id(zone, name, type), name=name, type=type, data=data, @@ -221,10 +217,8 @@ def update_record(self, record, name=None, type=None, data=None, extra=None): record=record, name=name, type=type, data=data, extra=extra ) - id = ":".join((self.RECORD_TYPE_MAP[type], name)) - return Record( - id=id, + id=self.to_default_id(record.zone, name, type), name=name, type=type, data=data, @@ -262,7 +256,7 @@ def ex_create_multi_value_record(self, name, zone, type, data, extra=None): ET.SubElement(change, "Action").text = "CREATE" rrs = ET.SubElement(change, "ResourceRecordSet") - ET.SubElement(rrs, "Name").text = name + "." + zone.domain + ET.SubElement(rrs, "Name").text = zone.hostname(name) ET.SubElement(rrs, "Type").text = self.RECORD_TYPE_MAP[type] ET.SubElement(rrs, "TTL").text = str(extra.get("ttl", "0")) @@ -280,13 +274,11 @@ def ex_create_multi_value_record(self, name, zone, type, data, extra=None): self.connection.set_context({"zone_id": zone.id}) self.connection.request(uri, method="POST", data=data) - id = ":".join((self.RECORD_TYPE_MAP[type], name)) - records = [] for value in values: record = Record( - id=id, + id=self.to_default_id(zone, name, type), name=name, type=type, data=value, @@ -338,12 +330,7 @@ def _update_multi_value_record(self, record, name=None, type=None, data=None, ex rrs = ET.SubElement(change, "ResourceRecordSet") - if record.name: - record_name = record.name + "." + record.zone.domain - else: - record_name = record.zone.domain - - ET.SubElement(rrs, "Name").text = record_name + ET.SubElement(rrs, "Name").text = record.hostname ET.SubElement(rrs, "Type").text = self.RECORD_TYPE_MAP[record.type] ET.SubElement(rrs, "TTL").text = str(record.extra.get("ttl", "0")) @@ -363,12 +350,7 @@ def _update_multi_value_record(self, record, name=None, type=None, data=None, ex rrs = ET.SubElement(change, "ResourceRecordSet") - if name: - record_name = name + "." + record.zone.domain - else: - record_name = record.zone.domain - - ET.SubElement(rrs, "Name").text = record_name + ET.SubElement(rrs, "Name").text = record.zone.hostname(name) ET.SubElement(rrs, "Type").text = self.RECORD_TYPE_MAP[type] ET.SubElement(rrs, "TTL").text = str(extra.get("ttl", "0")) @@ -399,12 +381,7 @@ def _post_changeset(self, zone, changes_list): rrs = ET.SubElement(change, "ResourceRecordSet") - if name: - record_name = name + "." + zone.domain - else: - record_name = zone.domain - - ET.SubElement(rrs, "Name").text = record_name + ET.SubElement(rrs, "Name").text = zone.hostname(name) ET.SubElement(rrs, "Type").text = self.RECORD_TYPE_MAP[type_] ET.SubElement(rrs, "TTL").text = str(extra.get("ttl", "0")) @@ -528,9 +505,8 @@ def _to_record(self, elem, zone, index=0): extra["weight"] = int(weight) extra["port"] = int(port) - id = ":".join((self.RECORD_TYPE_MAP[type], name)) record = Record( - id=id, + id=self.to_default_id(zone, name, type), name=name, type=type, data=data, diff --git a/libcloud/test/dns/test_route53.py b/libcloud/test/dns/test_route53.py index d7fed5d04b..27b0286155 100644 --- a/libcloud/test/dns/test_route53.py +++ b/libcloud/test/dns/test_route53.py @@ -196,7 +196,7 @@ def test_create_record_zone_name(self): self.assertIn("127.0.0.1", xml) self.assertIn("0", xml) - self.assertEqual(record.id, "A:") + self.assertEqual(record.id, "A") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) self.assertEqual(record.type, RecordType.A) @@ -208,7 +208,7 @@ def test_create_TXT_record(self): """ zone = self.driver.list_zones()[0] record = self.driver.create_record(name="", zone=zone, type=RecordType.TXT, data="test") - self.assertEqual(record.id, "TXT:") + self.assertEqual(record.id, "TXT") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) self.assertEqual(record.type, RecordType.TXT) @@ -220,7 +220,7 @@ def test_create_TXT_record_quoted(self): """ zone = self.driver.list_zones()[0] record = self.driver.create_record(name="", zone=zone, type=RecordType.TXT, data='"test"') - self.assertEqual(record.id, "TXT:") + self.assertEqual(record.id, "TXT") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) self.assertEqual(record.type, RecordType.TXT) @@ -232,7 +232,7 @@ def test_create_SPF_record(self): """ zone = self.driver.list_zones()[0] record = self.driver.create_record(name="", zone=zone, type=RecordType.SPF, data="test") - self.assertEqual(record.id, "SPF:") + self.assertEqual(record.id, "SPF") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) self.assertEqual(record.type, RecordType.SPF) @@ -244,7 +244,7 @@ def test_create_SPF_record_quoted(self): """ zone = self.driver.list_zones()[0] record = self.driver.create_record(name="", zone=zone, type=RecordType.SPF, data='"test"') - self.assertEqual(record.id, "SPF:") + self.assertEqual(record.id, "SPF") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) self.assertEqual(record.type, RecordType.SPF) @@ -258,7 +258,7 @@ def test_create_TXT_record_escaped(self): record = self.driver.create_record( name="", zone=zone, type=RecordType.TXT, data='test "with"' ) - self.assertEqual(record.id, "TXT:") + self.assertEqual(record.id, "TXT") self.assertEqual(record.name, "") self.assertEqual(record.zone, zone) self.assertEqual(record.type, RecordType.TXT) From 3a7c27fea10d7aeda5017ed41d8046272f416c8c Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 3 Apr 2025 11:27:25 -0500 Subject: [PATCH 51/52] dns: gandi_live: Leverage {to,from}_default_id() --- libcloud/dns/drivers/gandi_live.py | 9 +++++---- libcloud/test/dns/test_gandi_live.py | 4 +--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libcloud/dns/drivers/gandi_live.py b/libcloud/dns/drivers/gandi_live.py index 09df141a5d..76631bc956 100644 --- a/libcloud/dns/drivers/gandi_live.py +++ b/libcloud/dns/drivers/gandi_live.py @@ -146,15 +146,16 @@ def list_records(self, zone): """ def get_record(self, zone_id, record_id): - record_type, name = record_id.split(":", 1) - action = "{}/domains/{}/records/{}/{}".format(API_BASE, zone_id, name, record_type) + zone = self.get_zone(zone_id) + rparts = self.from_default_id(zone, record_id) + action = "{}/domains/{}/records/{}/{}".format(API_BASE, zone_id, rparts.name, rparts.type) try: record = self.connection.request(action=action, method="GET") except ResourceNotFoundError: raise RecordDoesNotExistError( value="", driver=self.connection.driver, record_id=record_id ) - return self._to_record(record.object, self.get_zone(zone_id))[0] + return self._to_record(record.object, zone)[0] def create_record(self, name, zone, type, data, extra=None): self._validate_record(None, name, type, data, extra) @@ -420,7 +421,7 @@ def _to_record_sub(self, data, zone, value): priority, value = value.split() extra["priority"] = priority return Record( - id="{}:{}".format(data["rrset_type"], data["rrset_name"]), + id=self.to_default_id(zone, data["rrset_name"], data["rrset_type"]), name=data["rrset_name"], type=self._string_to_record_type(data["rrset_type"]), data=value, diff --git a/libcloud/test/dns/test_gandi_live.py b/libcloud/test/dns/test_gandi_live.py index 2f7a66ca9c..909bfaa7a4 100644 --- a/libcloud/test/dns/test_gandi_live.py +++ b/libcloud/test/dns/test_gandi_live.py @@ -169,9 +169,7 @@ def test_list_records(self): def test_get_record(self): record = self.driver.get_record(self.test_zone.id, "A:bob") - # [0] /api/v5/domains/example.com/records/bob/A - # [1] /api/v5/domains/example.com - sent = GandiLiveMockHttp.history.pop(0) + sent = GandiLiveMockHttp.history.pop() self.assertEqual(sent.method, "GET") self.assertEqual(sent.url, "/api/v5/domains/example.com/records/bob/A") From fcaf1130de34ab65a8b046d432f5b04b28e515a9 Mon Sep 17 00:00:00 2001 From: Aaron Sierra Date: Thu, 3 Apr 2025 11:38:38 -0500 Subject: [PATCH 52/52] dns: rackspace: Leverage common host naming code Drop RackspaceDNSDriver._to_partial_record_name() and RackspaceDNSDriver._to_full_record_name() in favor of the common support provided by the Record and Zone classes. --- libcloud/dns/drivers/rackspace.py | 44 ++--------------------------- libcloud/test/dns/test_rackspace.py | 24 ---------------- 2 files changed, 3 insertions(+), 65 deletions(-) diff --git a/libcloud/dns/drivers/rackspace.py b/libcloud/dns/drivers/rackspace.py index e0edd9c53d..479712e0bf 100644 --- a/libcloud/dns/drivers/rackspace.py +++ b/libcloud/dns/drivers/rackspace.py @@ -292,7 +292,7 @@ def create_record(self, name, zone, type, data, extra=None): # name is "bar.foo.com" extra = extra if extra else {} - name = self._to_full_record_name(domain=zone.domain, name=name) + name = zone.hostname(name) data = {"name": name, "type": self.RECORD_TYPE_MAP[type], "data": data} if "ttl" in extra: @@ -314,8 +314,7 @@ def update_record(self, record, name=None, type=None, data=None, extra=None): # attribute must always be present. extra = extra if extra else {} - name = self._to_full_record_name(domain=record.zone.domain, name=record.name) - payload = {"name": name} + payload = {"name": record.hostname} if data: payload["data"] = data @@ -552,7 +551,6 @@ def _to_zone(self, data): def _to_record(self, data, zone): id = data["id"] fqdn = data["name"] - name = self._to_partial_record_name(domain=zone.domain, name=fqdn) type = self._string_to_record_type(data["type"]) record_data = data["data"] extra = {"fqdn": fqdn} @@ -563,7 +561,7 @@ def _to_record(self, data, zone): record = Record( id=str(id), - name=name, + name=fqdn, type=type, data=record_data, zone=zone, @@ -586,42 +584,6 @@ def _to_ptr_record(self, data, link): record = RackspacePTRRecord(id=str(id), ip=ip, domain=domain, driver=self, extra=extra) return record - def _to_full_record_name(self, domain, name): - """ - Build a FQDN from a domain and record name. - - :param domain: Domain name. - :type domain: ``str`` - - :param name: Record name. - :type name: ``str`` - """ - if name: - name = "{}.{}".format(name, domain) - else: - name = domain - - return name - - def _to_partial_record_name(self, domain, name): - """ - Remove domain portion from the record name. - - :param domain: Domain name. - :type domain: ``str`` - - :param name: Full record name (fqdn). - :type name: ``str`` - """ - if name == domain: - # Map "root" record names to None to be consistent with other - # drivers - return None - - # Strip domain portion - name = name.replace(".%s" % (domain), "") - return name - def _ex_connection_class_kwargs(self): kwargs = self.openstack_connection_kwargs() kwargs["region"] = self.region diff --git a/libcloud/test/dns/test_rackspace.py b/libcloud/test/dns/test_rackspace.py index 94c8382273..dd81261b93 100644 --- a/libcloud/test/dns/test_rackspace.py +++ b/libcloud/test/dns/test_rackspace.py @@ -400,30 +400,6 @@ def test_delete_record_does_not_exist(self): else: self.fail("Exception was not thrown") - def test_to_full_record_name_name_provided(self): - domain = "foo.bar" - name = "test" - self.assertEqual(self.driver._to_full_record_name(domain, name), "test.foo.bar") - - def test_to_full_record_name_name_not_provided(self): - domain = "foo.bar" - name = None - self.assertEqual(self.driver._to_full_record_name(domain, name), "foo.bar") - - def test_to_partial_record_name(self): - domain = "example.com" - names = [ - "test.example.com", - "foo.bar.example.com", - "example.com.example.com", - "example.com", - ] - expected_values = ["test", "foo.bar", "example.com", None] - - for name, expected_value in zip(names, expected_values): - value = self.driver._to_partial_record_name(domain=domain, name=name) - self.assertEqual(value, expected_value) - def test_ex_create_ptr_success(self): ip = "127.1.1.1" domain = "www.foo4.bar.com"