From b2e226e3a81e29bb7178dd3a46ec39de5cb140a9 Mon Sep 17 00:00:00 2001 From: Mattia Codato Date: Mon, 14 Jun 2021 12:23:40 +0200 Subject: [PATCH] Implementation of live_modification ref #2341 --- application/forms/KickstartForm.php | 19 +++ .../locale/de_DE/LC_MESSAGES/director.mo | Bin 161839 -> 163566 bytes .../locale/de_DE/LC_MESSAGES/director.po | 42 ++++++ .../locale/it_IT/LC_MESSAGES/director.mo | Bin 148601 -> 150273 bytes .../locale/it_IT/LC_MESSAGES/director.po | 44 +++++- library/Director/Core/CoreApi.php | 87 +++++++++++ library/Director/Daemon/BackgroundDaemon.php | 1 + library/Director/Daemon/LiveCreation.php | 103 +++++++++++++ library/Director/Db.php | 15 +- .../IcingaModifiedAttribute.php | 84 +++++++++++ .../IcingaObjectModifications.php | 26 ++++ .../Director/IcingaConfig/IcingaConfig.php | 9 ++ .../Director/Objects/DirectorActivityLog.php | 38 ++++- library/Director/Objects/IcingaHost.php | 137 ++++++++++++++++++ library/Director/Objects/IcingaObject.php | 101 ++++++++++++- ...ingaObjectLiveModificationAvailability.php | 55 +++++++ library/Director/Objects/IcingaService.php | 65 +++++++++ .../LiveModificationResetResolver.php | 131 +++++++++++++++++ .../Renderer/ConfigHealthItemRenderer.php | 11 ++ .../Director/Web/Widget/ActivityLogInfo.php | 13 ++ schema/mysql-migrations/upgrade_174.sql | 26 ++++ schema/mysql.sql | 21 ++- schema/pgsql-migrations/upgrade_174.sql | 28 ++++ schema/pgsql.sql | 25 +++- 24 files changed, 1069 insertions(+), 12 deletions(-) create mode 100644 library/Director/Daemon/LiveCreation.php create mode 100644 library/Director/DirectorObject/IcingaModifiedAttribute.php create mode 100644 library/Director/DirectorObject/IcingaObjectModifications.php create mode 100644 library/Director/Objects/IcingaObjectLiveModificationAvailability.php create mode 100644 library/Director/Resolver/LiveModificationResetResolver.php create mode 100644 schema/mysql-migrations/upgrade_174.sql create mode 100644 schema/pgsql-migrations/upgrade_174.sql diff --git a/application/forms/KickstartForm.php b/application/forms/KickstartForm.php index 7bc667d00..b1cfd797b 100644 --- a/application/forms/KickstartForm.php +++ b/application/forms/KickstartForm.php @@ -87,6 +87,22 @@ public function setup() array($this->getElement('HINT_ready')) ); + $this->addElement('checkbox', 'liveModification', array( + 'label' => $this->translate($this->translate('Live Modification enabled')), + 'required' => false, + 'value' => $this->config()->get('liveModification', 'enabled') + )); + + $this->addDisplayGroup(['liveModification'], 'liveModificationField', [ + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'dl')), + 'Fieldset', + ), + 'order' => 50, + 'legend' => $this->translate('Live modification') + ]); + return; } @@ -304,6 +320,9 @@ protected function storeResourceConfig() $config->setSection('db', array('resource' => $value)); + $liveModificationEnabled = $this->getValue('liveModification'); + $config->setSection('liveModification', array('enabled' => $liveModificationEnabled)); + try { $config->saveIni(); $this->setSuccessMessage($this->translate('Configuration has been stored')); diff --git a/application/locale/de_DE/LC_MESSAGES/director.mo b/application/locale/de_DE/LC_MESSAGES/director.mo index 55d63027fab795556512da25c1f48f86f95c80c5..94a9c68865cb46a10e62aaed9683ccf5d23120c1 100644 GIT binary patch delta 31789 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 delta 30178 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 z%0A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 delta 28041 zcmZwQ1$b3Q;HTkpZvr8N0KuK$8r&_oyA!+=cXuxC6eqYBcPLierMSCGDNaj~0{i{W z47>by_dL((J3ezp&bE948sh&FDCXqY|#6RL9BD!Eq8` zPK<@+k!+{FwJ8P=Z-sHO6Gq3Om>efyQe2I3@F*t2>qxsk=N*Ay5~6o>oKQ@JYM`LC zG)5y{4b@P6EPy>Q2-l(pzQcMM)$u!wjUk;(eri;DIP#26V@yQ<&PW0&NLb(|IL;pH z6-+|m8E4dgDSqJQVBO$hDc zI0=at!^~J0Q{oU*M~hHXzY{}nKgP!ks1E-`4fJ2sz(cy4rOApKSRPdQ5~%X^(5I2K zBoKnbP$Qj={vO9!iu;ML?B+O|uzhzk@)SKBr#SH<*c=C9dc1>qFltZ7sf2}b435Hj z7}Cpe!muqW9@&feSKt*1+C=4g+qFiGv_5J;oiG*-M9shi)E=3Js<*)AFGrQ%hT1bn zP)~jaW8+O5e}USJpL#R@eguO1n1(}84J1N!kO8&Ha-jxL47CZXq6XLwHFJY){uI<6 zS&Z>;4eC_v$AowZHT5rS`ez>jH5{w28DUD)8sSzmU0DDj~ zaTfLDS8e(|)LwXpYAM#DOR-Qh_wxXgJ{U9U{EsJ~0c=Hed>Ym9Lkz{Ym>1&?H1P_U zns_@*gcEG~GSq1~it6Zz&G#F`G7?XQ$+0xn!=KzZ>%Wsg2@G%Y;Cmv;( z<21ppxC+l=ZtOqYapvQCY>ib%X#SWvWb979k-UcSH~blsjiL+=!FG5SQ()oIW{*Um zPrJMY0r!}pHrGJZT24l-@nTfD6{xA-jM{vcQJd(l&3}!_h=0Z)Of<$USz6RgmBDgY z3)SA7F^pfkdl?DZ13NGpp2I-AZsQM89lby`^aTSkXsj7veAGElih4g}wDIhy_KRS2 ztcE%*5$KN{#xj4Z(3J!g?1LKlIMfV8qBhwI48Z;P8jqvu?Hp$YvLBUy5;f2ZsJ-$4 zHM6nDn}Mf9wHJofv8<1PDnz12wj5Pq2gbl-s3*RJn)(N*HHvLO>O2VnJ+zi9C)o6H^l3IMH#^;5pP@_y^VDXN-kW zCz+W`fH8?@LN#0fGhkNDThmxRjy!O;J8SsKsM}w#Zfc&8z#{C-$_7Ea1J#SS5X6ahZ?cpQqw?k%t<^u>i9KB z8gROx9$*w|56#5FxCqnXE!2$o{c4Pj8c=d{zyD_^kdTD(sLj$0{jn3O!S1LI2A~Ew z0aM`$RQVIutEh61Q4jDh>Hz|lnHfoiYOg42KxHvH`_HLGKvUfWwT4|$1L}`@(y6G1 z7h@FMY~wpH9`O^XCAfp?;2+e$zM|^ISZ-!2Ija3UsN-G?eQK~Q0X0+^lX{q1R09K7 zm^b1$3?sf2)8Tp4z&@Zl4p?cv0VPN6{tBoEYK+<=T~K>#0H(xI7!8-NWc`y7*g%5T zlzEK zX8s!yXi9?K&AU)fe%<=i`X1Gx{~EId$x%;`230;QR>DH4wI7LE`x&Tu8?YQ6$Ltug z*34819|0ApgR0mGbKoG+#-aAiTbHJlMO@Z1=P;iws^fErjm zjENmk57Hky;Ak8F2ZQwa@7-h?h>7VbkO);F9AjZ+RD+FB1L%tCa1?4FlW{z!W-jVM zGHo$SlM6L-)lo}Q7e8ZX)Y7#74e8&RqyYYg^J(ZhY5;w=nHd;~>hKrT6U{{J`sElO z_n`K|MbxQzi?K0eyBT0=)Psa!C>F(3*aY35{|6J`1Id|>wXw(!zA)ls)D*u!b@UN+ zzGLm=9e`<2Gxi=cGVtWTo8wh>mr3t}svn6faEXmq-ffnu{%+=9Q`m|GJy}=u;$SR@ z!!RT6Ms1$Qs3&`Yaquf@%|rGWQ=-}lLk+YD#>48Uep;gYnSmPEs6EWT8kkCg8eD{l zaSMjvY5WE6qFx-G_nPvvuq98t9JNV<_nXZXA6Y$TD7MA|2N($D7GebPEeCnqVyr{v zOrqdz2leC^P)qR;)$u1A_Z~J6kO=kUS@lpjhZndP3Adl3_UJ zKzHgf4e?RPfPBts0(#=zs5jeHOo*RR9mhRxI!uUqq70}f%4sc$QHa+1l7{ zeNZzt*18Zo6W@wGJU({n2?h9R^rRVa>{I4ArL~sCB2;LG>R=wKgZ0*37)bm$s^JSZ zz0_$l@am}aW|#xdq8`|DhW7&fJIM%WP4Z$f?0|)FHLBq!7z^KECiI*&4P-%0aal}? z5g3ZSP)iVrS#d6EhK{4!JAags40AhO)!wduXr2O z<{V_>Q!zR5m8i8oiotjrGqRS?F*fn|m(4Lxf0^~yrpiP@2j`W@{08dWhhH(DrWLU$@ph;gTXBW?Pe$Mn3EHLiP*0fZD!;A3 z!Kf!Xg6;4jM#E~?jP&7uCQiRQh?;8ed24jlWO>{eYVKkb7o#XGC?F3$-~5V;roGd9ej* zW@lkA{W}W?XiC>wci93bQ4OC%ZPKTxfxSX?@C8$1)cYo$9yP%1s1A!`8LWjG=qwDy zO{n910i)8t^MF7Se1@8uC=X1-2~p=eKdRwMr~x#xwnHsZAJn;CfZ8i7Q8Ts$HPBr) zei(IZPho0&fIdBW)Q9HNDLy76-UZd*1oXp5Oo($(9c)G|!5-^L)Kp)!@fWE2pRg|a zKQhOyAvPu68#O~$9x?xV!e=C`#jlth*E}{2+(dQs7(?+LY9``6F;9>V(-SX`YOpf~ z;85#WRQppEILQUr&671aaZWG+!&0G zGf?Fhp`LUt2I77jKZ$DRj`b<}6Muu7(f6J}VgjrFGGDb0VN&8RFb@VkHx-JZIx2;F zf@-LSTcb8r7u0DPiP{VEFb!@()w_&Zg8QfkdX4V+4}4*Edt%fY7DP>PWzb=9bI{zODXi9=#nTqK#6Y($`uZMc# zCaBHS1~tWfQEUDSs^i5N3pb!T-iI3KNgRwZQ5&#?(hG)6HvoLP-{2^)xk>C z6YoKd_ylSoXKng*)XY7`*!U6ELG(9f$&#Q3md2XX8jc!Rl{d_PRssX+~fKF2%|C4L9QCcRW3o`kTSyW6Xhl|1s~ARj7WhV>%3Y&;G)&_sqXW z+>eBEoa3>mH9G&V`80fi+U@Z_7{f3d@iJHtd)V}Kr~w|qfei$!~-jNMF=Ie?<-SKV&IX#~PyQjX^EpNz}*eU7PP0>~a1go)ZgE zKRLgF4yS*oek_l(7iZ#7#bbNi-vQsipDDOK#N!N51%8t3fV1Oy+;2<)2}}p2P&3gA z6JaF&gd4CG<>MywI2FBYa{gQ=JL!j$c-$XC|3UZX|04X-Pir251+X_}pyCGHK>TVl zkNYimOmd@t3Xe08bRSm4FW3{yr}VhHeh>B|ejk%!<4`m8eK8mD)u@5o4&~=By((ie z?|RqA#~@6CdO@VO@!S}PcsOd4)wJpTQLpAvs5jn3)YQ+$5L}OXL+(e-%q3L&FH!A( zO6BvoZ?u5a9;Yi$9D@3=8J5Q5zVXJR8kmAQzsszv5Fc615~T@|kycS=3t3!!@`UL$O1CGoT4rmH1_>f$0i(oYL4E z=ix5QhY4q{en70(@+DOgE|!pP{(trkAN!tj@op`QJd`|>J;3v z@fWCa?@a|3b>xrt@4|VKDqc(LUsvqA10xGxyHNv&%z93Lf zy2Zx-Kvg`0YUm1TX&&43Z>UogP};m`;v$E^$%J}SWjQdETn*43yf-G&tNjXy!H={MArCn#r5M_SYal|iLfL-p4f-QWMU zBB0IWvjujdrs^1`!0XnJsB@mEyh+c3p~Nd^PiZ2x0?MfhaHGFL`~^2EQ)Wi9cHiYafah;?13q3m=33*-WNNt z9>%O`2HFa>cRHaqU0>85nTXm0%X|d1>DHqPZng3Ks3|&zda`?{rFxB8nop=12&!e` zp_rL?PEfmsbkttfZBxlP!CoU3(&vQo`5#bQq-CsvGI$jP4?azwXSI( z0cwqNq4q>Q)Qt2;bvPciCl=ZKeW-yxM15Aou4iT>AEws%uSh^o&=K{{9)_CoF{lP- z+Vr)knb?fHL!2Y1JrdNwoRU14i+Bmt6ZSw&{RGsEEJBsvj+O8pW}<&5EW%7(Jxon} z1ZKb$s5L!{IyRqCPoBD=`KD72b*%bhGhBlzAKb{})WiZ<5yzoERWD;@%-GoD{uJF4 zeXN0VmVlltu!)(*|iA3Tdyu|iYxgTzeiNBj_2-L2gV&nTz zQ+^7yH@;#8jN8WSnZ~Hi*8z1}#-Z9>fkC**#`mB$_c0#SX!^<))LGtdZCzNJm?fGX!hosw~=V>lNz6Ta;P60n>1p(hiJqt@JA|70i>L>DhI*insCrR5*z+HcKt2+x zpq^kjYQz&z6=&J_3e;3?!2`Gl_31XGqxsO9g&N2fRK3%vO?C^*@PKhUnfFD5&gK{n z#3(xd%LwR6*Puqe4Ydc3q8hq}TI;8%8Tf)4aQ-glGouq~;A>G&xD{*RLDUo{>}uWv zDN!?-8`XYkjIZ-w*JgA;H89XR7WD*?s18n9FJmdh4D4fQ75WIc{5e-|sGUk{J_OQ!0m zO*sO!lrvF#Ybk2&SEFWnhyMIk$K^N)?i8cm(O)ndrs-+kd^J%W_eE{0NvL=KGSnBA z6R3_Jp$7g2)uGeNEI|OOTrAY9IWubSl!BZyqJwQ#q`Acpq6X_YHzJTJ^5zTlkd0r=X?Z8l5idCWAZ-y&Ify;D&9an*<;j` zzeP0=t*{zbT&#X~M~hO85Ob!0gIR=ug5y)G<4T z8}U49%7zd0IJ6w^mC#H6FbpGq*7~R!X@T0^oly->!;E+UwO9T^O}Tf3S+dyH z%&4U;jYV|+n-Wlkx#+HuEwCTe(M=nFiW-pLNV62Nu?_L`sF|CH+LY5!=YBh?qkX6) zyMfv}f1#E#$|%0}>-;Arpee13n(Fqb0S!lWI0x(C3e1ULQRQ=tHkLuXSRzoTr#))3 zPDY)Em8b!3M-6N@>IHNdeW3)d5YRFFfSQ4XV~pw0ePdZmqh2g^P*2(ob8* zPk0+Opa-a_eug>OeE#E1{e0t1dTG>5HW|d7af+FOm4@OPUY@Qe9PN*dIF`}qi{qD+aSP#rEut=)dqlb^*X zcpFvmA!;W6#QgXl=D|D@&97qHqT0KITEcIr_F_*mrbPAQ%SJ#0D36-z>ZpbrU~TMx zTAKZ+CAomwT+gsNdMBF?qgtq$i$r}^tU_9be28-jrsE!IuHA_$zbz1sZN1$eE3Tohys0Uex+9Uh0rq2ISn~`Li z=_o7yLV=Q~g6B}@_$ul}^bR%PuQor$bTbp_P&3#IH4q=F-gwN2%TY6Z7B#S|n4JEd zTLiSJKBLwsV20V%2~cYvih3_(L*3Ei7vlP`(&sk=weW*=15B0uSf*#5pz)Zvs%{H&_m#8OBJcsZ5 z^le`O>kK_km-4M%lQ8`ENU)GnThT7s3Ry|D$;;~~^B{0p@w zg6Es>h*_`+@s}8ZRTh{BU4hzDTNZHsHFYOQ(39LoeF)uM$fpzWphf2Ue%Hn3*X)Z? zGj$%%;B6aUx5P~GPU|t$@w$v&e2sbW9qPrCX{p&O4SfXkWIv;hOHb6uMx&NyzI7XF zGo3}fg8xDdJld~jjgz73mqyjEhkE69#H=_ORc{Z@!W*dfhOg%`GnH#`1y8&iwM$zO z>4}|iFFwSsxO62AQ!c?OkNYpH6wKuCu8BH^&5;N9 zIo%0p#KTcjI~(<+%TQ0WA9a2&qSpQ|RQdO)j$*7e-)56xQ{oNq5bnU`IB1=D;`Hmy zOzcBH(qCYp&i@+%YUnfSm;`MwYm*o?6RA<>H8X0HX2)V!8nroxq1JQ->H)5!K87D- zc8synEJYF2`=l9aZ?wiho&O;O)bKdeURbCM+=zPe-%$fRZ@rJ2p|_}xf;XA3V!2Vr zZ58y2^dbjqv)Rh0BULEq1LRZsV;ycuoPn(gc9|tfgE~F6c5(jO5@=0=cI7#n@c=c# ze^JLU=5DhYGoY5H5bk9cmA2{r`^`B|h#F`TRQ=Sb&w_%e4$GpBVFaec-acC(67{LK z)W&zA26P-%@dkFsSEwaualqq*VqYwZvr!FSMa|$ps3(ti(EJ%uE!4~%ME4DO$m9O2 z*}g0U^hCc|_u@$6$58{Se%PFfHmHt!q4vgj)RRS`_Rdn9zQLydj#EfKhB|HykC<{z zQ8U^esqb@!6VRrZY!aMBsI^*$n#yfh3-@9bjC<7lQmHxCBz_UYFvBtP6I30{MSLCV z_}#-$4B<#9Js0M{me@h(e+GeSB>0_R({XHS;S%g~((L+Vr_8y{g8KB!g_^Mfs8dlA zwNy1Q4z@r|buU!8eK!9DYSZ3CJ>Wg1>-;|>pf&yv_0Eoc+N@z7RKZ%PDQ|@8pf&1! z(GPWwC!z*+1~rqHPy>5p{TtPe-x;&^F;Fj}%;?iD%}GE7%3~(1VdMQ#4Gu?jI1Ov! zO4LCA!&Df0*33Xj)RNUkZL&tFJ<|_WZW3zOZ$j;@^Jh8#+6>P~&=mTevw!N1n!0e* zE4cz{lhw27El@Mk5!G-X>ul7kbr%l8%UBTWoHz9+qMm#XY7Z*cVsJ#)1TA~HW z()yeo1hlqCtrt)o-9f#{UZVCu@OATsONyGp0@gC97feljjdd|Smb+no@aTk^>LsW* z;$hSt@V}{ceSF>%(1;SFMxGnhaWT{`Zj74p{+J!7p$2%!dI9xud>=K{Uoazv+%f|$ zgj&ja=*|r4fxBTe`gaBq(B>M2d2upoPaH*Us;j6Oc!8SA&o&->+muU+8cwL8q_&qdA9Hq^il-}9NZIYok|_7-aDUZEQByKkN-6gA?!s0NBz zE2GLspk}ZWhGBoKgDbHvenTDax)03nj0T}j%@ZGi<^%#Bnx9zO;#T5WA9qQDh$EhuPqFE!H`p=M*Xxhi7~?-Rd!ajy;}{P`&1lSLW@a*>X0W`~*NTAFW;m9` z9jFGKzsy&#WT@j)0(GofU=|#UDz_Df;d$(ab)TER8+II56EF9|%+Nd3-UxVU{^&Is zzS8-BL?9CxmtL8r_=c^C$9nB?nqhBjk5^GsTl$T8l5Uul_++etdr+q*##^)NOX6wb zEm3lNYn;{5Se&IvR(X+U-~aA7TN__sKk2XKYCP zS1g8}&t`KK$CAV&u{mDHs#xSdzB$p+IBY=t&KJ{8!LQ~+tTXyDQs6ZKmWQ{X*Zrk& zfY<9zZ53=p#eS&o30E*3zQmju&(G`lQ?D#$!%F^MM+4~>#q0hB#@)Dp`0S`&=NlFZ z@Vb9Ps!%kqbAs|$0=+(`5rM@)rsHohyzY6l*UpqH=J zvAynJF!~b5>+Y43alP(TFTqrlKZa%S88)Y0j(A?@3h^oNz3w05w@cvjx_{;JRRXWJ zg3~Rb*PXhz*oSzDL|*rs&Q6?1{5IWx?u^(rrK zZGd{uv_qYq0X_n%I1_a|{y@E%?pgoDl*HfIct}!HE-`8#*-#x+wl+q+>pS6Sp13dS z#pO+I1{Q?ziN`^`8GRXTpai;aGK{JQP{*bxhT;U&ldeMz_%Z6+Y?2gS_roWpH5+Pg z6h$pTXKaF#QOEZ+&cPHZy?Ryi@Bb4BBH5Fd?t;x#t?1nOA5 zK(>MtCzTm+eALHz80vjc4pZYm)PpTV&De5Gqeb6NKvQ%RgYgj-!gr|8fNZHvg-WOf z8llRyuy(ciJ{zBe7s;QGdgAVB%;{N#n)2lh!WQ+Vz6TUVbx_$Ffoi`E>Op&02cmBP2_p%_V6*t8H=8GR2Cw@o zl%&WPV&^WNq1@(7W@d(EHtxaFq<=(xh7`-Hw-7f0ri4< zot5*i&+G6ouluTOf>VgEN4;=LW;4gK0c!JI!y*`x-5kfNs1Kj#mmP_NjUIGFg5LcA%7dkcG=yTrqac-?QmF^jTkN&kisq$dkE?exYL#3x`9^50-) z;=ULq%!u-%rnoxlMKl`q)odPWGpLJt{?lfvsT|_PAE!5`w5A`C7UedhK z;-lJ2j(XrQWV8C5f&|oXdDNS)3hF&j2lc&R3~G08Mn61+>fjh^w_ipL;1R0Bm#Be# zw0=XqAEJ~p1B`}>XTSiR|I7sR=F5fps#F&Bgw0VMx3}p7keTDJPMY*tsGo3Fp=NBi zO+Swbi9bNi+-FpK!KKXr6QX7!HOAEW&qY9A9!sD`Ru%OGHBIvJ~cu&-T zbm{o%x?^yf+Ke_#Z3;~<*Fw@-6TZuxl<;b9^{+oAcS_U4N5W+p!B@;g+GTECZw>#? zKi6oqTzvQZAJ%UV2T znarxw%Z9Zq6Sz0fXnh(ONnTCjow>hX0SvA#ncM@pbwtjQR+am$ZKSIT+72F2W)A6{uq4TAxfgRErM%t?x}K2F z-gEkMdl+OmX@8O5l(_cSLGpdqNZ>c=&NNjdLtm5lwT}BzIZfJqZXHWqW5`c#2lE?c zW|9}it!p#sd<%1mqE1Z|^4oJCBzzFh<3F}6K_@olu510N8Ibo*(ZM$CT`z}*qIQIh*ciTpGTGdWU?kU{6$-l(io=Upb6V6Cj zZ@%^1)u=NLqtihLZa(mw5L@;a(sPra4u@bx>IPHhH+{NqCGdfSL$)wwoCT!m`o{f+ z^#7>T#HOVpJcRO7xOF|_UP9h%H)j69k9#wT` zjg_?xZXts&D9&g);^W-C_7iSQ`br!BM%f_3A4y;CHpD(MMV-gEnDjr$&qTRDxHl2! zhaUGTVavxMJ%x6)u2WCTSgQ!BFD9oBX2`iHoUlj*KzHuM)pPg*~JdCT*5&C@OiWxqtn! zCmYU6x~_}V+r^!O@O#u1kNTatFHmMB@zvawO&8;{JZMq2O0rIlAp!c{do) zN%BsjuHBfF{GTX0jC(VAHAvq*@?d=y$$8s5k5h=t{6$YJYjuX*6#)u5^tjR zY~>r6f&A&D>B>dEF1`@EKbE=AA7B$lkkOGw2UCGB!tS4Nr6PQSwEcLR+g&R-r7+hl zyv==;`=;$=3F&?|UuD9$f99S=JNLNLaofx1{-~tLRx0PR8I2jxhW}H@{b9mm2dT0% zY@WUi&7fW~+((BOa2VzOA-s|B_iM-ZfVD4muG7Xwe*fs?rSJ&uWZd5`eh2mcT)|W- zOqqXiEcronq+hQ@;6l>ds!?h^#P8Qm8{R~I!ynSu+P*4k{=>=8H&|WaWSpf@epU0o zuT-|;Ve(>e`wO3O+f^Y|0zL2f&8(38uRgZYK@#kkv6>_J&wUr4Wy-)wn(v)E0!d*t&AE~gM4#jVJXgH9ssMtOdc zbI$4ge~ir5B#T_4HFN!mlgNhz$WJMP0dbn-jltK|Q9T_KW}yeQPIP2Nh%9IHZ9Ogna#8uYheYCuIZ3yMxiO6X|ieLkZW!0cuO@UzK|t30=t0)s%+5Uok24k$V{l zHEgLc;C2i`jSl+xjL;ty?^KS}?M`v7+=n^qlvwjHjsX@SJ6m@vQprEmfo z(wmT9)9JrMul)-bM1(bL)Cc+rv%JiBDM9OsvW6`$*&KsE~vEcQW)_i2=5v3IKTx{wVbxZKm4e?gQV^Ah7;j1=XbSG_xel>WS2)`M3mT_k$ zaS0Xn5!SVddnn;(R4&HdpIg5L89}+;-25iqiBI|7r0c3lT6%njXGkkK_03Q2e`3S= zlTuN5HC;emGsyjk+L44$a<}AuK`mWBlb4WkMF`iSOjqIo+)=4NA9dBIYz4}E;7&`K ziKO=;Eg$}5+ZjYUm(N*3!g4oE0h<}0%z}jTlYXB{Rk-Vs*349L|1jqd^7Gj8s(zDj zD4wP4G&(NIy-pci18uq2gwK$^lQLxqPxaH5;=0D|N8?4vcuqJ9zPFWPDbI#~CH*rE z{CK@3uP5pM_ch9LEP5}pCfOT+O+*U%SmW%8(#B6OEF1{#eL$3v{W>3 z#FkBs&$wTbrYp6<{a2b597y^un|Fl{m;5l$oaFne+04PV!Kk)CZ{iPsDBQ)C8&4xo zsF;uVYQps?x0LWZJ&nEoBYvKE1Qz4oZ1XY@9&O7cHu{|PBn}~xn9lyQgZNB^Mcijd zZ$$bDoBo=z$w)6wdQW_VrAgDZ$PRX%wG;We%2*R(G&bpR@~hc0TeSXJh$J90C7qO~ z!erZEcG7KK=Q()+q<_Ug(huS$Ze3x-%ad1#`v&1|c*$10Lz&K`4K+obXXNSHNPaBt z{omJLiN90$0CyOLy4eQ$k{(9dY3|G1y4rBp;jUpD`-J6`VXr{qV+nV(ZJZ~bhr1~Q z(KUszt|a8U{U0a6pU4r+iIKKoKioz^Kc?;bb=x-DkA$6+Z-*x-ciz_do&3k7SK#*9 zv@ckb!Jo2qm)Zx3$se(~|M896Bs~4zgLO8Qni1yDcHBR#c}j!h2=`EB-CUQs2asNg z_#`|=2LnkPMOfDh+So(*8BXTzO8yw`)r8H(^G78?S6uE$3WpQ#k85n9(b$*{&XPZY z@LKY+Vl~ogAir#OuMYgPC3(rIzmsqm+wMK`yAZ!j*#$VC^!=3EPrClM2~J(xz%3GW z{fPx^+HX|)eg#vp0qM0!_}eDx|4dV!^y#)UC1>M4M!i36+EMBxq|F4j{Z-a4l=Jnm z9aOSLp;9U`@}rl68*KV>+t>_iPZ~W=crRrh+qC4QP3FGNz1F6EqJDGQTZVN=tAq8( z&yDl6|GWfx(8ymT`Y;c1U4Ij9VS-L!^3stW*EXu;^tN0g(q`InGjJ4l1M;p=KR36o z{@lH2Lsu^H9&vx6UOj!q)s==jHidt+g_O2{LJcSsgsVw^#66okI#WK6^h=bPM|?B( z!M{neHTPWAD4+%HPj7q{{)U}O_ z2*Q=_tEoW2#dyTW@ekDDa$NcSwHVWF_6Wg>#l!;Avvwo=RLjx;F zs7*pq)LVW7OtFLcO!yiN47HVsx_`@CR^tw&v-ad~NDRhtUY{G|W@B-m9qbA34quY7 z{gUuB?m+T&&7rX@xZTE+Ql>9;vfFrR^1f0g2X_c*8_C~8+98u~PW&Jum*7PoD^rQBe`p`_KdX;m>EcSp+Z(Kn8^1bUKi zoCfz%@c(y}A#o0OW6GxCPH7t(Pg*+aH0R#M{epCVmPprJ%DpD-0e3CJDaac|`1`ej zz(joy`hJyRw6C~FaBtq!v0s7BEth4^wApj^bhOPMuZG9n?0eoIU~|CVB?C4O|2!vf zTe&En+9e_vm-S?gyjIpTJu;}GH)UjGIZv#}4dpzywrwr%N#NNQQPI<|#I_IPJae*d z3)|&s9zC*OLr>7QV@Eut6K-pG+tVO0a@iA4%xxc@c$!ArHutsXR`hKfKYNl_iF{Gs Wn=7(X1@D%~=oP&MBO@w$m;W!g5oH+w diff --git a/application/locale/it_IT/LC_MESSAGES/director.po b/application/locale/it_IT/LC_MESSAGES/director.po index ceb49b7e8..0f38e06c7 100644 --- a/application/locale/it_IT/LC_MESSAGES/director.po +++ b/application/locale/it_IT/LC_MESSAGES/director.po @@ -7428,4 +7428,46 @@ msgid "e.g. " msgstr "per es." msgid "start using" -msgstr "inizia ad utilizzare" \ No newline at end of file +msgstr "inizia ad utilizzare" + +msgid "scheduled" +msgstr "Schedulata" + +msgid "succeeded" +msgstr "Applicata con successo" + +msgid "failed" +msgstr "Errore nell'aggiornamento live" + +msgid "impossible" +msgstr "Impossibile effettuare l'aggiornamento live" + +msgid " (This change cannot be automatically applied on monitored object. Please deploy to align configuration.)" +msgstr " (Questa modifica non puo' essere automaticamente applicata all'oggetto monitorato. Prego effettuare un deploy manuale della configurazione.)" + +msgid "There are %d pending live modifications" +msgstr "Ci sono %d modifiche live in attesa di essere applicate" + +msgid "Template is not supported by Live Modification" +msgstr "Il Template non è supportato dall'aggiornamento live" + +msgid "Live Modification not supported: the host belongs to a modified group" +msgstr "L'aggiornamento live non è supportato: è stata modificata l'appartenenza dell'host ad uno o più gruppi" + +msgid "The host property %s is not supported by Live Modification" +msgstr "La proprietà %s dell'host non è supportata dall'aggiornamento live" + +msgid "Service related to Host Template is not supported by Live Modification" +msgstr "Un servizio associato ad un template non è supportato dall'aggiornamento live" + +msgid "Object not supported by Live Modification" +msgstr "Oggetto non supportato dall'aggiornamento live" + +msgid "Live Modification enabled" +msgstr "Aggiornamento live abilitato" + +msgid "Live Modification" +msgstr "Aggiornamento live" + +msgid "Disabled Services are not supported by Live Modification" +msgstr "I servizi didabilitati non sono supportati dall'aggiornamento live" diff --git a/library/Director/Core/CoreApi.php b/library/Director/Core/CoreApi.php index 8f792293f..5ec6166d2 100644 --- a/library/Director/Core/CoreApi.php +++ b/library/Director/Core/CoreApi.php @@ -5,8 +5,11 @@ use Exception; use Icinga\Exception\NotFoundError; use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\IcingaModifiedAttribute; +use Icinga\Module\Director\Exception\JsonEncodeException; use Icinga\Module\Director\Hook\DeploymentHook; use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorActivityLog; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Objects\IcingaCommand; use Icinga\Module\Director\Objects\DirectorDeploymentLog; @@ -16,6 +19,11 @@ class CoreApi implements DeploymentApiInterface { + const OBJECT_PLURALITY = [ + 'Host' => 'hosts', + 'Service' => 'services' + ]; + protected $client; protected $initialized = false; @@ -914,4 +922,83 @@ protected function client() return $this->client; } + + public function sendModification(IcingaModifiedAttribute $modifiedAttribute) + { + $activityId = $modifiedAttribute->getProperty('activity_id'); + try { + $activityLog = DirectorActivityLog::load($activityId, $this->db); + } catch (NotFoundError $e) { + return false; + } + + $action = $activityLog->getProperty('action_name'); + $objectType = $modifiedAttribute->getProperty('icinga_object_type'); + $objectName = $modifiedAttribute->getProperty('icinga_object_name'); + try { + $modification = $modifiedAttribute->getModifiedAttributes(); + } catch (JsonEncodeException $e) { + return false; + } + + switch ($action) { + case 'create': + $result = $this->createObject($objectType, $objectName, $modification); + break; + case 'modify': + $result = $this->modifyObject($objectType, $objectName, $modification); + break; + case 'delete': + $result = $this->deleteObject($objectType, $objectName); + break; + default: + return false; + } + + return $result && $result->succeeded(); + } + + protected function createObject($type, $name, $attrs) + { + try { + return $this->client()->put( + 'objects/' . self::OBJECT_PLURALITY[$type] . '/' . $name, + (object)array( + 'package' => 'director', + 'attrs' => $attrs + ) + ); + } catch (Exception $e) { + return false; + } + } + + protected function modifyObject($type, $name, $attrs) + { + try { + return $this->client()->post( + 'objects/' . self::OBJECT_PLURALITY[$type] . '/' . $name, + (object)array( + 'package' => 'director', + 'attrs' => $attrs + ) + ); + } catch (Exception $e) { + return false; + } + } + + protected function deleteObject($type, $name) + { + try { + return $this->client()->delete( + 'objects/' . self::OBJECT_PLURALITY[$type] . '/' . $name, + (object) array( + 'cascade' => true + ) + ); + } catch (Exception $e) { + return false; + } + } } diff --git a/library/Director/Daemon/BackgroundDaemon.php b/library/Director/Daemon/BackgroundDaemon.php index 34cc28b76..d877a6ba7 100644 --- a/library/Director/Daemon/BackgroundDaemon.php +++ b/library/Director/Daemon/BackgroundDaemon.php @@ -88,6 +88,7 @@ protected function initialize() ->register($this->jobRunner) ->register($this->logProxy) ->register(new DeploymentChecker($this->loop)) + ->register(new LiveCreation($this->loop)) ->run($this->loop); $this->setState('running'); } diff --git a/library/Director/Daemon/LiveCreation.php b/library/Director/Daemon/LiveCreation.php new file mode 100644 index 000000000..e74eff23a --- /dev/null +++ b/library/Director/Daemon/LiveCreation.php @@ -0,0 +1,103 @@ +addPeriodicTimer(5, function () { + if ($this->db) { + $this->run(); + } + }); + } + + /** + * @return IcingaModifiedAttribute[] + */ + public function fetchPendingModifications() + { + return IcingaModifiedAttribute::loadAll( + $this->db, + $this->db->getDbAdapter() + ->select()->from('icinga_modified_attribute') + ->where('state != ?', 'applied') + ->order('state') + ->order('id') + ); + } + + public function applyModification(IcingaModifiedAttribute $modifiedAttribute) + { + try { + return $this->db->getDeploymentEndpoint()->api()->sendModification($modifiedAttribute); + } catch (Exception $e) { + return false; + } + } + + public function run() + { + foreach ($this->fetchPendingModifications() as $modification) { + $activityLogStatus = null; + $activityId = $modification->get('activity_id'); + if ($activityId !== null) { + $activityLog = DirectorActivityLog::load($activityId, $this->db); + } + + if ($this->applyModification($modification)) { + if ($modification->get('state') === 'scheduled_for_reset') { + $modification->delete(); + } else { + if ($activityLog->get('live_modification') === + DirectorActivityLog::LIVE_MODIFICATION_VALUE_SCHEDULED) { + $activityLogStatus = DirectorActivityLog::LIVE_MODIFICATION_VALUE_SUCCEEDED; + } + $modification->set('state', 'applied'); + $modification->set('ts_applied', DaemonUtil::timestampWithMilliseconds()); + $modification->store(); + } + } else { + $activityLogStatus = DirectorActivityLog::LIVE_MODIFICATION_VALUE_FAILED; + $modification->delete(); + } + + if ($activityId !== null && $activityLogStatus !== null) { + $activityLog->set('live_modification', $activityLogStatus); + $activityLog->store($this->db); + } + } + } + + /** + * @param Db $connection + * @return \React\Promise\ExtendedPromiseInterface + */ + public function initDb(Db $connection) + { + $this->db = $connection; + + return resolve(); + } + + /** + * @return \React\Promise\ExtendedPromiseInterface + */ + public function stopDb() + { + $this->db = null; + + return resolve(); + } +} diff --git a/library/Director/Db.php b/library/Director/Db.php index af859ba05..3fbc7a32e 100644 --- a/library/Director/Db.php +++ b/library/Director/Db.php @@ -96,6 +96,17 @@ protected function setClientTimezoneForPgsql() $db->query($db->quoteInto('SET TIME ZONE INTERVAL ? HOUR TO MINUTE', $this->getTimezoneOffset())); } + + public function countPendingLiveModifications() + { + $db = $this->db(); + $query = 'SELECT COUNT(*) FROM icinga_modified_attribute'; + $query .= $db->quoteInto(' WHERE state = ?', 'scheduled'); + $query .= $db->quoteInto(' OR state = ?', 'scheduled_for_reset'); + + return (int) $db->fetchOne($query); + } + public function countActivitiesSinceLastDeployedConfig(IcingaObject $object = null) { $db = $this->db(); @@ -296,7 +307,7 @@ public function fetchActivityLogEntryById($id) $sql = 'SELECT id, object_type, object_name, action_name,' . ' old_properties, new_properties, author, change_time,' . ' UNIX_TIMESTAMP(change_time) AS change_time_ts,' - . ' %s AS checksum, %s AS parent_checksum' + . ' %s AS checksum, %s AS parent_checksum, live_modification' . ' FROM director_activity_log WHERE id = %d'; $sql = sprintf( @@ -341,7 +352,7 @@ public function fetchActivityLogEntry($checksum) $sql = 'SELECT id, object_type, object_name, action_name,' . ' old_properties, new_properties, author, change_time,' . ' UNIX_TIMESTAMP(change_time) AS change_time_ts,' - . ' %s AS checksum, %s AS parent_checksum' + . ' %s AS checksum, %s AS parent_checksum, live_modification' . ' FROM director_activity_log WHERE checksum = ?'; $sql = sprintf( diff --git a/library/Director/DirectorObject/IcingaModifiedAttribute.php b/library/Director/DirectorObject/IcingaModifiedAttribute.php new file mode 100644 index 000000000..2cec38f81 --- /dev/null +++ b/library/Director/DirectorObject/IcingaModifiedAttribute.php @@ -0,0 +1,84 @@ + null, + 'activity_id' => null, + 'action' => null, + 'modification' => null, + 'ts_scheduled' => null, + 'ts_applied' => null, + 'icinga_object_type' => null, + 'icinga_object_name' => null, + 'state' => null + ]; + + public function getUniqueKey() + { + return $this->get('icinga_object_type') . '!' . $this->get('icinga_object_name'); + } + + public function getModifiedAttributes() + { + $modifications = $this->get('modification'); + if (is_null($modifications)) { + return []; + } + return Json::decode($modifications); + } + + public static function prepareIcingaModifiedAttributeForSingleObject(IcingaObject $object, $activityId = null) + { + // TODO: throw if not supported + $action = self::getActionFromObject($object); + $resolvedProperties = self::getObjectModifiedPropertyFromAction($object, $action); + return IcingaModifiedAttribute::create([ + 'activity_id' => $activityId, + 'action' => $action, + 'modification' => $resolvedProperties, + 'ts_scheduled' => DaemonUtil::timestampWithMilliseconds(), + 'icinga_object_type' => $object->getIcingaObjectType(), + 'icinga_object_name' => $object->getIcingaObjectName(), + 'state' => 'scheduled' + ]); + } + + protected static function getActionFromObject(IcingaObject $object) + { + if ($object->shouldBeRemoved()) { + $action = 'delete'; + } elseif ($object->hasBeenLoadedFromDb()) { + $action = 'modify'; + } else { + $action = 'create'; + } + + return $action; + } + + protected static function getObjectModifiedPropertyFromAction(IcingaObject $object, $action) + { + $properties = new \stdClass(); + if ($action === 'modify') { + $properties = $object->getModifiedProperties(); + } elseif ($action === 'create') { + $properties = $object->toApiObject(true, true); + } + return Json::encode($properties); + } +} diff --git a/library/Director/DirectorObject/IcingaObjectModifications.php b/library/Director/DirectorObject/IcingaObjectModifications.php new file mode 100644 index 000000000..29a9285b1 --- /dev/null +++ b/library/Director/DirectorObject/IcingaObjectModifications.php @@ -0,0 +1,26 @@ +objectType = $objectType; + $this->objectName = $objectName; + } + + public function addModification(IcingaModifiedAttribute $modifiedAttribute) + { + foreach ($modifiedAttribute->getModifiedAttributes() as $key => $value) { + $this->modifications[$key] = $value; + } + } +} diff --git a/library/Director/IcingaConfig/IcingaConfig.php b/library/Director/IcingaConfig/IcingaConfig.php index 78776bea6..03cc99181 100644 --- a/library/Director/IcingaConfig/IcingaConfig.php +++ b/library/Director/IcingaConfig/IcingaConfig.php @@ -11,6 +11,7 @@ use Icinga\Module\Director\Db; use Icinga\Module\Director\Hook\ShipConfigFilesHook; use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Resolver\LiveModificationResetResolver; use Icinga\Module\Director\Util; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaZone; @@ -473,6 +474,14 @@ protected function generateFromDb() ->createFileFromDb('scheduledDowntime') ; + $resolver = new LiveModificationResetResolver($this->connection); + $activityId = $this->connection->fetchActivityLogIdByChecksum($this->getLastActivityChecksum()); + $modifiedAttributes = $resolver->fetchAppliedModificationsBeforeAndIncludingActivityId($activityId); + $resolver->scheduleAttributesToBeReset($modifiedAttributes); + foreach ($modifiedAttributes as $attribute) { + $attribute->delete(); + } + PrefetchCache::forget(); IcingaHost::clearAllPrefetchCaches(); diff --git a/library/Director/Objects/DirectorActivityLog.php b/library/Director/Objects/DirectorActivityLog.php index 6b4bcc3e5..23151a981 100644 --- a/library/Director/Objects/DirectorActivityLog.php +++ b/library/Director/Objects/DirectorActivityLog.php @@ -11,6 +11,12 @@ class DirectorActivityLog extends DbObject { + const LIVE_MODIFICATION_VALUE_SCHEDULED = 'scheduled'; + const LIVE_MODIFICATION_VALUE_SUCCEEDED = 'succeeded'; + const LIVE_MODIFICATION_VALUE_FAILED = 'failed'; + const LIVE_MODIFICATION_VALUE_IMPOSSIBLE = 'impossible'; + const LIVE_MODIFICATION_VALUE_DISABLED = 'disabled'; + protected $table = 'director_activity_log'; protected $keyName = 'id'; @@ -28,6 +34,7 @@ class DirectorActivityLog extends DbObject 'change_time' => null, 'checksum' => null, 'parent_checksum' => null, + 'live_modification' => self::LIVE_MODIFICATION_VALUE_IMPOSSIBLE ]; protected $binaryProperties = [ @@ -106,11 +113,17 @@ public static function logCreation(IcingaObject $object, Db $db) 'object_type' => $type, 'new_properties' => $newProps, 'change_time' => date('Y-m-d H:i:s'), - 'parent_checksum' => $db->getLastActivityChecksum() + 'parent_checksum' => $db->getLastActivityChecksum(), ); $data['checksum'] = sha1(json_encode($data), true); $data['parent_checksum'] = hex2bin($data['parent_checksum']); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $data['live_modification'] = self::LIVE_MODIFICATION_VALUE_IMPOSSIBLE; + } else { + $data['live_modification'] = self::LIVE_MODIFICATION_VALUE_DISABLED; + } + static::audit($db, array( 'action' => 'create', @@ -119,7 +132,9 @@ public static function logCreation(IcingaObject $object, Db $db) 'new_props' => $newProps, )); - return static::create($data)->store($db); + $activityLog = static::create($data); + $activityLog->store($db); + return $activityLog; } public static function logModification(IcingaObject $object, Db $db) @@ -142,6 +157,11 @@ public static function logModification(IcingaObject $object, Db $db) $data['checksum'] = sha1(json_encode($data), true); $data['parent_checksum'] = hex2bin($data['parent_checksum']); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $data['live_modification'] = self::LIVE_MODIFICATION_VALUE_IMPOSSIBLE; + } else { + $data['live_modification'] = self::LIVE_MODIFICATION_VALUE_DISABLED; + } static::audit($db, array( 'action' => 'modify', @@ -151,7 +171,10 @@ public static function logModification(IcingaObject $object, Db $db) 'new_props' => $newProps, )); - return static::create($data)->store($db); + $activityLog = static::create($data); + $activityLog->store($db); + + return $activityLog; } public static function logRemoval(IcingaObject $object, Db $db) @@ -172,6 +195,11 @@ public static function logRemoval(IcingaObject $object, Db $db) $data['checksum'] = sha1(json_encode($data), true); $data['parent_checksum'] = hex2bin($data['parent_checksum']); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $data['live_modification'] = self::LIVE_MODIFICATION_VALUE_IMPOSSIBLE; + } else { + $data['live_modification'] = self::LIVE_MODIFICATION_VALUE_DISABLED; + } static::audit($db, array( 'action' => 'remove', @@ -180,7 +208,9 @@ public static function logRemoval(IcingaObject $object, Db $db) 'old_props' => $oldProps )); - return static::create($data)->store($db); + $activityLog = static::create($data); + $activityLog->store($db); + return $activityLog; } public static function audit(Db $db, $properties) diff --git a/library/Director/Objects/IcingaHost.php b/library/Director/Objects/IcingaHost.php index d9a179458..4eb39d8ad 100644 --- a/library/Director/Objects/IcingaHost.php +++ b/library/Director/Objects/IcingaHost.php @@ -11,6 +11,8 @@ use Icinga\Module\Director\IcingaConfig\IcingaConfig; use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; use Icinga\Module\Director\Objects\Extension\FlappingSupport; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use Icinga\Util\Translator; use InvalidArgumentException; use RuntimeException; @@ -18,6 +20,19 @@ class IcingaHost extends IcingaObject implements ExportInterface { use FlappingSupport; + const LIVE_DENIED_PROPERTIES_EDIT_MODE = [ + 'zone_id', + 'has_agent', + 'master_should_connect', + 'accept_config', + 'disabled', + ]; + + const LIVE_DENIED_PROPERTIES_CREATION_MODE = [ + 'has_agent', + 'disabled', + ]; + protected $table = 'icinga_host'; protected $defaultProperties = array( @@ -282,6 +297,36 @@ public function export() return (object) $props; } + public function toApiObject($resolved = false, $skipDefaults = false) + { + $plainObj = parent::toApiObject($resolved, $skipDefaults); + + $propertiesToBeRemoved = array( + 'id', + 'object_name', + 'object_type', + 'disabled', + 'check_command_id', + 'check_period_id', + 'event_command_id', + 'zone_id', + 'command_endpoint_id', + 'has_agent', + 'master_should_connect', + 'accept_config', + 'api_key', + 'template_choice_id' + ); + + foreach ((array)$plainObj as $prop => $value) { + if (in_array($prop, $propertiesToBeRemoved)) { + unset($plainObj->$prop); + } + } + + return $plainObj; + } + /** * @param $plain * @param Db $db @@ -618,4 +663,96 @@ public static function loadWithApiKey($key, Db $db) return current($result); } + + protected function canBeAppliedLive() + { + $liveModificationAvailability = new IcingaObjectLiveModificationAvailability(); + $liveModificationAvailability->setResult(true); + + if ($this->isTemplate()) { + $liveModificationAvailability->setErrorMessage( + Translator::translate('Template is not supported by Live Modification', 'director') + ); + $liveModificationAvailability->setResult(false); + } elseif ($this->hasBeenModified()) { + $modifiedProperties = $this->getModifiedProperties(); + if ($this->hasBeenLoadedFromDb()) { + // check if groups has been modified + if ($this->groups()->hasBeenModified()) { + $liveModificationAvailability->setErrorMessage( + Translator::translate( + 'Live Modification not supported: the host belongs to a modified group', + 'director' + ) + ); + $liveModificationAvailability->setResult(false); + } + $this->checkModifiedDeniedProperty( + self::LIVE_DENIED_PROPERTIES_EDIT_MODE, + $modifiedProperties, + $liveModificationAvailability + ); + } else { + $this->checkModifiedDeniedProperty( + self::LIVE_DENIED_PROPERTIES_CREATION_MODE, + $modifiedProperties, + $liveModificationAvailability + ); + } + } + + return $liveModificationAvailability; + } + + protected function checkModifiedDeniedProperty( + $deniedProperties, + $modifiedProperties, + $liveModificationAvailability + ) { + foreach ($deniedProperties as $deniedProperty) { + if (array_key_exists($deniedProperty, $modifiedProperties)) { + $liveModificationAvailability->setErrorMessage(sprintf( + Translator::translate('The host property %s is not supported by Live Modification', 'director'), + $deniedProperty + )); + $liveModificationAvailability->setResult(false); + break; + } + } + } + + protected function deleteRelatedObjectsModification() + { + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($this) + ->getTemplatesFor($this, true); + foreach ($parents as $parent) { + $services = $parent->fetchServices(); + foreach ($services as $service) { + $id = $this->getId(); + $service->set('host', $id); + $properties = $service->getProperties(); + $newService = IcingaService::create($properties, $this->connection); + $newService->markForRemoval(true); + $newService->scheduleLiveModification(); + } + } + } + + protected function insertRelatedObjectsModification($activityLog = null) + { + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($this) + ->getTemplatesFor($this, true); + foreach ($parents as $parent) { + $services = $parent->fetchServices(); + foreach ($services as $service) { + $id = $this->getId(); + $service->set('host', $id); + $properties = $service->getProperties(); + $newService = IcingaService::create($properties, $this->connection); + $newService->scheduleLiveModification($activityLog); + } + } + } } diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php index ef8004dbb..032ef41b9 100644 --- a/library/Director/Objects/IcingaObject.php +++ b/library/Director/Objects/IcingaObject.php @@ -9,6 +9,7 @@ use Icinga\Exception\NotFoundError; use Icinga\Module\Director\CustomVariable\CustomVariables; use Icinga\Module\Director\Data\Db\DbDataFormatter; +use Icinga\Module\Director\DirectorObject\IcingaModifiedAttribute; use Icinga\Module\Director\IcingaConfig\AssignRenderer; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Db\Cache\PrefetchCache; @@ -20,6 +21,9 @@ use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use Icinga\Util\Translator; +use Icinga\Web\Notification; +use InvalidArgumentException; use LogicException; use RuntimeException; @@ -132,6 +136,9 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer protected static $tree; + /* @var IcingaObjectLiveModificationAvailability */ + protected $liveModificationAvailability; + /** * @return Db */ @@ -1470,6 +1477,57 @@ public function isApplyRule() && $this->get('object_type') === 'apply'; } + protected function canBeAppliedLive() + { + $liveModificationAvailability = new IcingaObjectLiveModificationAvailability(); + $liveModificationAvailability->setResult(false); + $liveModificationAvailability->setErrorMessage( + Translator::translate('Object not supported by Live Modification', 'director') + ); + return $liveModificationAvailability; + } + + protected function insertSingleObjectModification($activityLog) + { + $activityLogId = null; + if (! is_null($activityLog)) { + $activityLogId = $activityLog->getId(); + $activityLog->set('live_modification', DirectorActivityLog::LIVE_MODIFICATION_VALUE_SCHEDULED); + $activityLog->store($this->connection); + } + + $modifiedAttribute = IcingaModifiedAttribute::prepareIcingaModifiedAttributeForSingleObject( + $this, + $activityLogId + ); + $modifiedAttribute->store($this->connection); + } + + protected function insertRelatedObjectsModification($activityLog = null) + { + } + + protected function deleteRelatedObjectsModification() + { + } + + protected function scheduleLiveModification($activityLog = null) + { + // This check is needed when related objects are created + if (!$this->liveModificationAvailability) { + $this->liveModificationAvailability = $this->canBeAppliedLive(); + } + + if ($this->liveModificationAvailability->getResult()) { + $this->insertSingleObjectModification($activityLog); + if (!$this->shouldBeRemoved()) { + $this->insertRelatedObjectsModification($activityLog); + } + } else { + Notification::warning($this->liveModificationAvailability->getErrorMessage()); + } + } + /** * @throws NotFoundError * @throws \Icinga\Module\Director\Exception\DuplicateKeyException @@ -1496,6 +1554,12 @@ protected function beforeStore() if ($this->gotImports()) { $this->imports()->getObjects(); } + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $this->liveModificationAvailability = $this->canBeAppliedLive(); + if ($this->liveModificationAvailability->getResult() && $this->hasBeenLoadedFromDb()) { + $this->deleteRelatedObjectsModification(); + } + } } /** @@ -1505,8 +1569,11 @@ protected function beforeStore() */ public function onInsert() { - DirectorActivityLog::logCreation($this, $this->connection); + $activityLog = DirectorActivityLog::logCreation($this, $this->connection); $this->storeRelatedObjects(); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $this->scheduleLiveModification($activityLog); + } } /** @@ -1516,8 +1583,11 @@ public function onInsert() */ public function onUpdate() { - DirectorActivityLog::logModification($this, $this->connection); + $activityLog = DirectorActivityLog::logModification($this, $this->connection); $this->storeRelatedObjects(); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $this->scheduleLiveModification($activityLog); + } } public function onStore() @@ -1621,6 +1691,10 @@ protected function storeImports() public function beforeDelete() { $this->cachedPlainUnmodified = $this->getPlainUnmodifiedObject(); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $this->markForRemoval(true); + $this->liveModificationAvailability = $this->canBeAppliedLive(); + } } public function getCachedUnmodifiedObject() @@ -1630,7 +1704,10 @@ public function getCachedUnmodifiedObject() public function onDelete() { - DirectorActivityLog::logRemoval($this, $this->connection); + $activityLog = DirectorActivityLog::logRemoval($this, $this->connection); + if (IcingaObjectLiveModificationAvailability::isEnabled()) { + $this->scheduleLiveModification($activityLog); + } } public function toSingleIcingaConfig() @@ -2484,6 +2561,16 @@ public function hasCheckCommand() return false; } + public function getIcingaObjectType() + { + return $this->getType(); + } + + public function getIcingaObjectName() + { + return $this->getObjectName(); + } + protected function getType() { if ($this->type === null) { @@ -2762,6 +2849,14 @@ public function merge(IcingaObject $object, $replaceVars = false) return $this; } + public function toApiObject( + $resolved = false, + $skipDefaults = false + ) { + $plainObj = $this->toPlainObject($resolved, $skipDefaults); + return $plainObj; + } + /** * @param bool $resolved * @param bool $skipDefaults diff --git a/library/Director/Objects/IcingaObjectLiveModificationAvailability.php b/library/Director/Objects/IcingaObjectLiveModificationAvailability.php new file mode 100644 index 000000000..3355cb6ce --- /dev/null +++ b/library/Director/Objects/IcingaObjectLiveModificationAvailability.php @@ -0,0 +1,55 @@ +get('liveModification', 'enabled'); + + return $liveModificationEnabled === '1'; + } + + /** + * @return bool + */ + public function getResult() + { + return $this->result; + } + + /** + * @param bool $result + */ + public function setResult($result) + { + $this->result = $result; + } + + /** + * @return string + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * @param string $errorMessage + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + } +} diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php index e2ad831ec..c91540e1b 100644 --- a/library/Director/Objects/IcingaService.php +++ b/library/Director/Objects/IcingaService.php @@ -14,6 +14,7 @@ use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; use Icinga\Module\Director\Objects\Extension\FlappingSupport; use Icinga\Module\Director\Resolver\HostServiceBlacklist; +use Icinga\Util\Translator; use InvalidArgumentException; use RuntimeException; @@ -166,6 +167,19 @@ public function getUniqueIdentifier() } } + public function getIcingaObjectName() + { + if ($this->isApplyRule()) { + return 'host_name!' . $this->getObjectName(); + } + + if ($host = $this->get('host')) { + return "$host!" . $this->getObjectName(); + } + + throw new RuntimeException('Cannot determine an Icinga Object Name for a Service not bound to a host'); + } + /** * @return object * @throws \Icinga\Exception\NotFoundError @@ -422,6 +436,27 @@ public function toConfigString() } } + public function toApiObject($resolved = false, $skipDefaults = false) + { + $plainObj = parent::toApiObject($resolved, $skipDefaults); + + $propertiesToBeRemoved = array( + 'id', + 'object_name', + 'object_type', + 'use_agent', + 'host' + ); + + foreach ((array)$plainObj as $prop => $value) { + if (in_array($prop, $propertiesToBeRemoved)) { + unset($plainObj->$prop); + } + } + + return $plainObj; + } + /** * @return string */ @@ -798,4 +833,34 @@ public function setServiceGroupMembershipResolver(ServiceGroupMembershipResolver $this->servicegroupMembershipResolver = $resolver; return $this; } + + protected function canBeAppliedLive() + { + $liveModificationAvailability = new IcingaObjectLiveModificationAvailability(); + $liveModificationAvailability->setResult(true); + + if ($this->isTemplate()) { + $liveModificationAvailability->setErrorMessage( + Translator::translate('Template is not supported by Live Modification', 'director') + ); + $liveModificationAvailability->setResult(false); + } elseif ($this->hasBeenAssignedToHostTemplate()) { + $liveModificationAvailability->setErrorMessage( + Translator::translate( + 'Service related to Host Template is not supported by Live Modification', + 'director' + ) + ); + $liveModificationAvailability->setResult(false); + } + + if ($this->isDisabled()) { + $liveModificationAvailability->setErrorMessage( + Translator::translate('Disabled Services are not supported by Live Modification', 'director') + ); + $liveModificationAvailability->setResult(false); + } + + return $liveModificationAvailability; + } } diff --git a/library/Director/Resolver/LiveModificationResetResolver.php b/library/Director/Resolver/LiveModificationResetResolver.php new file mode 100644 index 000000000..c6fbf8c3e --- /dev/null +++ b/library/Director/Resolver/LiveModificationResetResolver.php @@ -0,0 +1,131 @@ +db = $db; + } + + /** + * @param IcingaModifiedAttribute[] $modifiedAttributes + * @return IcingaObjectModifications[] + */ + public function cleanModifications(array $modifiedAttributes) + { + $modifications = []; + foreach ($modifiedAttributes as $modification) { + if ($modification->get('action') === 'delete' || $modification->get('state') !== 'applied') { + continue; + } + $key = $modification->getUniqueKey(); + if (!isset($modifications[$key])) { + $modifications[$key] = new IcingaObjectModifications( + $modification->get('icinga_object_type'), + $modification->get('icinga_object_name') + ); + } + $modifications[$key]->addModification($modification); + } + + return $modifications; + } + + /** + * @return IcingaModifiedAttribute[] + */ + public function fetchAppliedModificationsBeforeAndIncludingActivityId($id) + { + return IcingaModifiedAttribute::loadAll( + $this->db, + $this->db->getDbAdapter()->select()->from('icinga_modified_attribute') + ->where('activity_id <= ?', $id) + ->order('id') + ); + } + + /** + * @param IcingaModifiedAttribute[] $modifiedAttributes + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function scheduleAttributesToBeReset(array $modifiedAttributes) + { + $cleanupModifications = []; + $performedModifications = $this->cleanModifications($modifiedAttributes); + foreach ($performedModifications as $modification) { + switch ($modification->objectType) { + case 'Host': + try { + $object = IcingaHost::load($modification->objectName, $this->db); + } catch (\Exception $e) { + break; + } + + $dummy = IcingaObject::createByType('host', [ + 'object_name' => $object->get('object_name'), + ], $this->db); + break; + case 'Service': + $icingaServiceName = explode('!', $modification->objectName); + try { + $host = IcingaHost::load($icingaServiceName[0], $this->db); + $object = IcingaService::load([ + 'object_name' => $icingaServiceName[1], + 'host_id' => $host->get('id') + ], $this->db); + } catch (\Exception $e) { + break; + } + + $dummy = IcingaService::createByType('service', [ + 'object_name' => $object->getObjectName(), + 'host_id' => $object->get('host_id') + ], $this->db); + break; + + default: + throw new \RuntimeException(sprintf( + 'Resetting attribute for %s is not supported', + $modification->objectType + )); + } + + // to handle the case when and object is created and deleted with live modification + // before a manual deploy + if (!isset($object)) { + continue; + } + + /** @var IcingaObject $object */ + foreach ($modification->modifications as $key => $value) { + $currentValue = $object->getResolvedProperty($key); + if ($currentValue !== $value) { + $dummy->set($key, $currentValue); + } + } + if (count($dummy->getModifiedProperties())) { + // TODO: api props only -> toApiObject -> remove object_name + $attribute = IcingaModifiedAttribute::prepareIcingaModifiedAttributeForSingleObject($dummy); + $attribute->set('state', 'scheduled_for_reset'); + $attribute->set('action', 'modify'); + $cleanupModifications[] = $attribute; + } + } + + foreach ($cleanupModifications as $modification) { + $modification->store($this->db); + } + } +} diff --git a/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php b/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php index 374fc5165..920fa0ae3 100644 --- a/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php +++ b/library/Director/Web/Navigation/Renderer/ConfigHealthItemRenderer.php @@ -102,6 +102,17 @@ protected function checkHealth() return; } + $pendingLiveModifications = $db->countPendingLiveModifications(); + if ($pendingLiveModifications > 0) { + $this->directorState = self::STATE_PENDING; + $this->count = $pendingLiveModifications; + $this->message = sprintf( + $this->translate('There are %d pending live modifications'), + $pendingLiveModifications + ); + return; + } + $pendingChanges = $db->countActivitiesSinceLastDeployedConfig(); if ($pendingChanges > 0) { diff --git a/library/Director/Web/Widget/ActivityLogInfo.php b/library/Director/Web/Widget/ActivityLogInfo.php index f2f4c889a..00f11d081 100644 --- a/library/Director/Web/Widget/ActivityLogInfo.php +++ b/library/Director/Web/Widget/ActivityLogInfo.php @@ -582,6 +582,19 @@ public function getInfoTable() ); } + $liveModification = $this->translate($entry->live_modification); + if ($entry->live_modification === 'failed' || $entry->live_modification === 'impossible') { + $liveModification .= $this->translate( + ' (This change cannot be automatically applied on monitored object.' . + ' Please deploy to align configuration.)' + ); + } + + $table->addNameValueRow( + $this->translate('Live Modification'), + $liveModification + ); + return $table; } diff --git a/schema/mysql-migrations/upgrade_174.sql b/schema/mysql-migrations/upgrade_174.sql new file mode 100644 index 000000000..fd617f432 --- /dev/null +++ b/schema/mysql-migrations/upgrade_174.sql @@ -0,0 +1,26 @@ +ALTER TABLE director_activity_log + ADD COLUMN live_modification ENUM('scheduled', 'succeeded', 'failed', 'impossible', 'disabled') NOT NULL; + +UPDATE director_activity_log SET live_modification = 'disabled'; + +CREATE TABLE icinga_modified_attribute ( + id BIGINT(20) UNSIGNED AUTO_INCREMENT NOT NULL, + activity_id BIGINT(20) UNSIGNED DEFAULT NULL, + state ENUM('scheduled_for_reset', 'scheduled', 'applied') NOT NULL, + action ENUM('create', 'delete', 'modify') NOT NULL, + icinga_object_type VARCHAR(64) NOT NULL, + icinga_object_name VARCHAR(255) NOT NULL, + modification MEDIUMTEXT NOT NULL, + ts_scheduled BIGINT(20) NOT NULL, + ts_applied BIGINT(20) DEFAULT NULL, + PRIMARY KEY (id), + FOREIGN KEY activity_log_id (activity_id) + REFERENCES director_activity_log (id) + ON DELETE RESTRICT + ON UPDATE CASCADE, + INDEX sort_idx (ts_scheduled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO director_schema_migration +(schema_version, migration_time) +VALUES (174, NOW()); diff --git a/schema/mysql.sql b/schema/mysql.sql index 87d04d99a..5d03df55a 100644 --- a/schema/mysql.sql +++ b/schema/mysql.sql @@ -41,6 +41,7 @@ CREATE TABLE director_activity_log ( change_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, checksum VARBINARY(20) NOT NULL, parent_checksum VARBINARY(20) DEFAULT NULL, + live_modification ENUM('scheduled', 'succeeded', 'failed', 'impossible', 'disabled') NOT NULL, PRIMARY KEY (id), INDEX sort_idx (change_time), INDEX search_idx (object_name), @@ -49,6 +50,24 @@ CREATE TABLE director_activity_log ( INDEX checksum (checksum) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE icinga_modified_attribute ( + id BIGINT(20) UNSIGNED AUTO_INCREMENT NOT NULL, + activity_id BIGINT(20) UNSIGNED DEFAULT NULL, + state ENUM('scheduled_for_reset', 'scheduled', 'applied') NOT NULL, + action ENUM('create', 'delete', 'modify') NOT NULL, + icinga_object_type VARCHAR(64) NOT NULL, + icinga_object_name VARCHAR(255) NOT NULL, + modification MEDIUMTEXT NOT NULL, + ts_scheduled BIGINT(20) NOT NULL, + ts_applied BIGINT(20) DEFAULT NULL, + PRIMARY KEY (id), + FOREIGN KEY activity_log_id (activity_id) + REFERENCES director_activity_log (id) + ON DELETE RESTRICT + ON UPDATE CASCADE, + INDEX sort_idx (ts_scheduled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CREATE TABLE director_basket ( uuid VARBINARY(16) NOT NULL, basket_name VARCHAR(64) NOT NULL, @@ -1884,4 +1903,4 @@ CREATE TABLE icinga_scheduled_downtime_range ( INSERT INTO director_schema_migration (schema_version, migration_time) - VALUES (173, NOW()); + VALUES (174, NOW()); diff --git a/schema/pgsql-migrations/upgrade_174.sql b/schema/pgsql-migrations/upgrade_174.sql new file mode 100644 index 000000000..255050d5e --- /dev/null +++ b/schema/pgsql-migrations/upgrade_174.sql @@ -0,0 +1,28 @@ +CREATE TYPE enum_live_modification_state AS ENUM('scheduled', 'succeeded', 'failed', 'impossible', 'disabled'); +CREATE TYPE enum_icinga_modified_attribute_state AS ENUM('scheduled_for_reset', 'scheduled', 'applied'); + +ALTER TABLE director_activity_log ADD COLUMN live_modification enum_live_modification_state NOT NULL; +UPDATE director_activity_log SET live_modification = 'disabled'; + +CREATE TABLE icinga_modified_attribute ( + id bigserial, + activity_id integer DEFAULT NULL, + state enum_icinga_modified_attribute_state NOT NULL, + action enum_activity_action NOT NULL, + icinga_object_type VARCHAR(64) NOT NULL, + icinga_object_name VARCHAR(255) NOT NULL, + modification MEDIUMTEXT NOT NULL, + ts_scheduled bigint NOT NULL, + ts_applied bigint DEFAULT NULL, + PRIMARY KEY (id), + FOREIGN KEY (activity_id) + REFERENCES director_activity_log (id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX icinga_modified_attribute_sort_idx ON icinga_modified_attribute (ts_scheduled); + +INSERT INTO director_schema_migration +(schema_version, migration_time) +VALUES (174, NOW()); \ No newline at end of file diff --git a/schema/pgsql.sql b/schema/pgsql.sql index a404acc7d..f113ded22 100644 --- a/schema/pgsql.sql +++ b/schema/pgsql.sql @@ -47,6 +47,8 @@ CREATE TYPE enum_sync_state AS ENUM( ); CREATE TYPE enum_host_service AS ENUM('host', 'service'); CREATE TYPE enum_owner_type AS ENUM('user', 'usergroup', 'role'); +CREATE TYPE enum_live_modification_state AS ENUM('scheduled', 'succeeded', 'failed', 'impossible', 'disabled'); +CREATE TYPE enum_icinga_modified_attribute_state AS ENUM('scheduled_for_reset', 'scheduled', 'applied'); CREATE DOMAIN d_smallint AS integer CHECK (VALUE >= 0) CHECK (VALUE < 65536); CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS ' @@ -87,6 +89,7 @@ CREATE TABLE director_activity_log ( change_time timestamp with time zone NOT NULL, checksum bytea NOT NULL UNIQUE CHECK(LENGTH(checksum) = 20), parent_checksum bytea DEFAULT NULL CHECK(parent_checksum IS NULL OR LENGTH(checksum) = 20), + live_modification enum_live_modification_state NOT NULL, PRIMARY KEY (id) ); @@ -98,6 +101,26 @@ COMMENT ON COLUMN director_activity_log.old_properties IS 'Property hash, JSON'; COMMENT ON COLUMN director_activity_log.new_properties IS 'Property hash, JSON'; +CREATE TABLE icinga_modified_attribute ( + id bigserial, + activity_id integer DEFAULT NULL, + state enum_icinga_modified_attribute_state NOT NULL, + action enum_activity_action NOT NULL, + icinga_object_type VARCHAR(64) NOT NULL, + icinga_object_name VARCHAR(255) NOT NULL, + modification MEDIUMTEXT NOT NULL, + ts_scheduled bigint NOT NULL, + ts_applied bigint DEFAULT NULL, + PRIMARY KEY (id), + FOREIGN KEY (activity_id) + REFERENCES director_activity_log (id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE INDEX icinga_modified_attribute_sort_idx ON icinga_modified_attribute (ts_scheduled); + + CREATE TABLE director_basket ( uuid bytea CHECK(LENGTH(uuid) = 16) NOT NULL, basket_name VARCHAR(64) NOT NULL, @@ -2201,4 +2224,4 @@ COMMENT ON COLUMN icinga_scheduled_downtime_range.merge_behaviour IS 'set -> = { INSERT INTO director_schema_migration (schema_version, migration_time) - VALUES (173, NOW()); + VALUES (174, NOW());