tar-lvm/0000755002342000234200000000000014042103062011362 5ustar baxicbaxictar-lvm/install/0000755002342000234200000000000013754302440013042 5ustar baxicbaxictar-lvm/install/doc/0000755002342000234200000000000013754042526013615 5ustar baxicbaxictar-lvm/install/doc/tar-lvm-deployment-scheme.png0000644002342000234200000004261313754042526021333 0ustar baxicbaxicPNG  IHDRR0bKGD IDATxwxTeߙIbHDTDXٵYе+ "%HS"^RS?:I&ו+ss晷k!tw,́h$'d2;!p% ; w8./;;%K<=9:BW1i|߽{%@jj*OknJ!\6q␷|}}Y`qy.CC ՗&0c }aaҿ~7o<{N8矯1+8JRB;gQQÇϝ;Cƅ4X6l`jIH.4IjB#F4ni׮ ӧOu_$eΝl۶ܮ]_}XBуB ;w~„ ^&ɛƍ̱"5__IRBz1c(nݺ2d{FFCry7o19sZ1 oڛx% !D=ONÇsa{f0]pww$5!?BԩJ_cQBB!\$5!.CB!IM!ː&eHRB2$ !pԄB |-V6vҟ5M#44Az14*!<]YgcѢE0Çge&M$&f_#~6-[]̶mIJJ+}ʶm/<˗g̀7j{&} FB4!wf= d W@PP`r$C__oc v6QIB41sS֭HNN#))[w1mڭuɳ[+L IM!Paa or~ܸx{{zF֬وc47IJ!D;vĉ71*e<<&9s&Y=:K/Knn> ΐ!,~g$IM!HźL|3_~In܎x uϧ7>H#IM!χZRk>/A:E\$ !X| 99y<*FHRB+$UyI\яB!\$5!.CB!IM!ː&eHRB2$ !AץKVB&ڵC~cTQunnB{o@Cǎ,X0T8sn$5!H.яDDFmQ[Grqx1{$*ľ}yż>该*'DK ykڭw8u2L[`pZBB ,{w`Ѣ=寲`{zzFNNY14F\ZF@AhPR<={iN<"#??…>۾}ZWK3% 3ٳ=Cc>22=ʈ+-3ɽ>ǹsq8OdcM:u^{g/Ю][y!:vlW*n݊㏻y饷z9kM={N<˵׶aѢh߾- Z#ٴ*fTsJ^5u)7yzܡCa~$3#f=t͞=J?9z$"=kSk=]z޽Xj).jg%K>~ ׽?8to<ǎ HBΥ,5iҀkf_ߣ4ج]t1SHMĬ&(M7MgI<*eΞ@Wx\b!hʕ_j:HHHoߞ;s<>,Ǐ&N51cFk{l~0a$fOcwQQugC:233Ytۃp3֭ȖzO.]cŊ;R;ǏqOә% 6q9y<ﰰ.#))>ojw!+?pp&Q7_?Ԅ@DDOxߺ`Q7:mQ&?޽+ݻw ##Ͼ}r==zxSaKPyWmF#Tq_P[-$5!ХKԔYfh_ݮoc2X~3{̛7{f- .w[x 翣jmy=xTm`պ]׮mlU=<^}EnJt:&O3,_R5Ln~Nc-<e nG/*) ~`=f*Yn@eT&D tުUFEos75?{)[ru_b-G-G^xE8 7tw8 x8]dZ d_u 'uZ kAAެ:[ kjf++<*ǎR8v94Ur҄&yyرS$4www<<_Y6s Ȱl,0æ}p;v’%)ZkJ99>}+ry>K^*|7 ܫBml6gH6 ֗aqj-+-L&TZ"??"h_=Wɒ1i %wW,UɐCϞy8q\2-Zs%9Pݿq\\",[2sNmKCI KZje{bMĒw(@>G@ -HMh1:PdLpʕ`.^l65k5N&^xjo.\w8ڼ{vANN^}ڢu g TT_vFF8qq^{m ]wm2cX\[XZ[۳g^E`ƌf>}RPPd^}>s";*:?;$G8CUA ?jHEڧV(H777͛Z帻3gcɒO x~fmٲ>H.ݙ9s*^^ۢ%..U֒JNNSD|z`t`эWPMlE_"υt`oG*,itޑYӡCjˍs#kl 8sFFZp^G`0qo?~zקcp3gΠәz5>}2h MN5*݋3*}\VߍQ.)_ch,Am0ͬ_ ~ %2aaL0ojM~TImmOټyݥ&g,1ܹSۿ@ڴiCԩ-fa;vcNGAAnnzrr曇к5zi Jd(| >B`ôfľ}yż>诬[eV1aHs*K~IM4I;F>|/g,Q߿Rٹs?))i4Cxxc01tz=xxx; 1Mb6pwa6CA777ڶdx{׽DX}Pl_|,Tm^K$LV=EEEQ.^L$''MSsf3zGN# NG``YYW0L\hʕ+deerEN:E# v`25O=uybw},yJ@-qFƖC̙{ٶm>̞]qӐ!+} DvvN͛w`2Y?k ҥD23ТE0jx`?*/SsfjZ}kE)_SY cǯ\1S.u=:&:s?% .]ps*a 5/5QV++Q^XoۘB+ݧVQQ6/[`ذ~DEP$`>P-^}}2sc/73w֮QZENN>g$L911"33͕+h;p7ӺuӭӾo_64!!-[rIbcOܹ ~~^}Q +/5Զ/% ̙…/6% `ٲ,[Ĭ`ʔ[46mgÆ0Mq\,fdP!8wL6>}3L?"5cc~9ݺE7O*8y=05c jtTR."::M>rrkUmkk4)Snfʔ/Wp &N 6#p99>g0flٲocqIjBؑhd  ƍǰaFzI=ٽ{|]0`@gz5I.RXb>Dm jA_E _'ð8zŋ-V?8?Jhb/jPHBz53grFHRkl' i! 88C5ִ8@ jR ->`0Mڼ5&xG@ڔxZs9')[T %Ԗ!k5Oٝ+Wrk+D  (D 9_d4jjKh }j~:ףjep`eF2Q;DYz V|>xݹIOO#?+[Zo)SV.N^e'  Eddd;f!?? ޸qKR2D}U8-؟@mRD>qq+W4Y۠/oh3pb2x7)((ǧ;j4c]/-0'j8fY1Œr*XyF]Ӊ(_y5ܳV99_'=h64 '88l{k>;Zok9s|!EE]Ҋ[ʺ:=% &}!V6QMp00&fES@:Wt@yQ|եܫZTTdFh%}M?zDo/fŊp]SxefXj jvCm5xDFkPM-++sM{2{+dG*\Sy@| gۨ79rT:Y`vwQ3Po%Mi>c=t r }ttdm4N…sؽŀ5j8uJᇟسg/\ 7tfM/!AI-0П{kW,g^]6s!+}3֭#>*q3ѣ? K|ƇV^l@4PF׿jn 8 ,*w h\_]@u 9vB"z Þinn۴~ӠA70h :u-[vu#Ftt4ݺuWxzZoO"bcr1Ο?TDaB7<Ν\BBnu(vw?s}mع*=z^^SMR}ܱ7s *[P5g9VLѹs>~~^K~W_ll6ҭVңch:vر3رÇpq6n\OQww eI``nnnEQQ!YYW(**tRSի@QQnnzC.G{jpRׯ!!^nmG oʕ$4ba!,r@k??^/,,آhGDi:EnɈvtҞ.]ʺ3ٹi$$$NC/!(FFQ?-C2-[[- IDAT^!IM1j`֮a1jj•;w.5enZn]{KzzVӧ/ &^sooOz=>>^t:<˫~[1ik$!\FzP֮aBrH\:/:uyyy`4YY٘fF#f3h+l!\VZtځGOӵk"#TVd]Mi۷=Z.T\ª&>Լ^`yXkI+'/yۙN1uj .]BG'_φw=~~fM{*,\vVPP̓•9̊"B;f<=Ue4//__IjBX$5!lhȐDD+J$ aC&A-%3Z>IjBH۶Y.qZZ& \%JM\a%п2QAIl*2SZ!ː7MLnn dYpIpՂry@߳.Խ͌ ^C!cAvo߱ {UYF 4e?  IM4yԨ_?ށ8 hZ9((tPY4'H8v*+ʮ}o^q 4 6naYd˖Ν;!j$}jBPJJ*/qb ؏4>|3EEMvcF֮B||ḹH;k,}%>}zraΝP5״ҥD>+zF߾]aUؿ0&ng a#EӮ]tb##DGfȐ^x{okk/ܹhzѳg;!DCHRAL>?rQ1̄{md}B"G&!!77|ڵ &yHRFo˖vl.BSt)t:EEF|}} '88-hjtyy%IFF0tz ߿3~~! a Ԅ\w]Ziii>}T/Q:pCmBj.Y44͌N+(ѱc Za4xЄS& hZJ}EDT9~E~ 74l "\! 77w׹&z ԄGsRU!!AYB8IjB#9z$f+9@ttU)&0p`_{!KeB a#_|&wtt&M$™2YB4"COLV…K'X]0dH $8$ը& ZYpN4#s R|| jXʼnzjH6ɽ(̔ljo2psiVhN ܹaJu:t?|TV17<4O`=j-U8̵hF$ aԛf0j4KQCkԣG7W_UԠ5T?",p+OyWTj*~su8:\'IjBM|tjd5o vLeffY~Ms`>5կ T=5 Q'IjBeҀ@N 6IUG.8?GDD8P jjz\[Ze ,'D$ Qy届沚Poܹ;wntە^@5]>LD5 !+Q FhX\eP62ìpUS+]5M#44Az14lh6ڢ7ܵk5V%,%7n;;pz3Q o9.?Jj%Irr\aa+Wc x;mh&/Q5oqD !!ָ]4SzOS2`)jeA%')ಣ?Բ;qڴ @NN.g? YMf~5DDGzz!!<ۣάYX6={4eQ|Z0jqlVVүHNNm}fpն*8A. 55dwmA#Z`?d׮XΞHvmس !Cz_Zgҭ[G}UΝgңG'~,?Kb0KÃQkfLk34>a׾}'5L&Z |5YRj.hT㭷e׮Xg ;wZ||XrGˣu~~>$%5?xaa oh=dj@TE1GU# i.ѴPד E$&^ĉMr_jlL.nJj $$~mM"6!!۽BV߿l s[c.@4BB8&ϟְ --WP+^Q VѲe(Çד&v cwj{ ŔSLŽt:F f/^IaaF Fkڙ}\|a6_ߕZ8j@_Tڻv 8HB+l@mmoȄ5z㭷evFzҲM&àgyGAhݺm۶f؛ޕ9XB+jݨU`&3ꃊ 4:nN׮8z4]v 2߫P~D1]1_N ,AZco"<ܺ/^ѣV'&Tmm*ME04y!Xeڛo>]ɨ뺟hziiY`q/О4d/ww<==.X{] 35m(j"[ђ|xyp"5Z8VXhw> ^^]&77ww&ʜ, 5U㏨%SQ\؀G%IMT+%%Ә^9bB3ɝQ{QVP9rul__ 5'HPl7ul SX$5QErr199owXY{ Ma3ǛZy?V Us<oQM(FHD))479ox U+کz n\,(^6 BMlE]5WI\c4|.)AtzIjTJJ111=YӴ7ܞQ&dJXpL:\s$q V &aA$R1A *z>O0~f?~M)0\IRd35Ma'6%΁1HEm&/(܅d2Y5fJQN}fl$TT-m0jby^ouI>wX;VQIjj*˗/3ZHcsAZZVP4`˖M34vSEt( AYBՔ]QK&y|jk6=;w[ힹ:&i:߄f 7@jΏCM~]ĉCg5rR944P=@f/55vJhl5<!ʂsG5-D%E 2;vE6 Vչs>No{;8j"_Zʽ8B3Iݼy|||nPN8矯1Kv8w/76qх.dV/SWa\]tZYwHsP[8{P[cB l4 j"ƎYTsq!M-66 6Z_a}w9^  ݴ}BXjiJ>s-$;؂٬&WwURS5:A՚3S F~eXPh4:U:tYOJVϡ*kDWMD"@p ϡgF:7zڻ :W !&Ϩ9lQΦ-jºRfOpf]QtcuR߻dl%Q{ IR*Vj7ՅP֬XZypR6$ B4kԄ3rGT؍Z>tuԄӣ6 ~O6 Q]TsC^sIM kYrz[ٺui$%u.MS.T $5ơOFr47VηG-_u<ԜLaa or~ܸx{{zF֬وc~}t{N\)@N.7P{lbs%wx.9ci!!ALxOR˓ɓo.m3g^^w޼?ݿ5ILpTC(v.SE5Q8ƂsP@f&55'bɓo/Ֆy&\g;ѧOwUCpDmQBGme|jp'AŚZ+ԴèU?.:P!$5a+:VC51ztBJ*3e#?YO񱱨&{Q}m(Ñ"VWi}JtQ}jˡ} |OY?Y$e}m*]CB4[Ԅ {U`bS}jU bIR2evYD!5%Z|@4%Nx ^XL̊ kNOMJZ6ykF%&+?(!TZVqqRSc%&l!²@fΣysm6F[BB ,{w`Ѣ=寲`{zzFNNY14F*k1c,klæIMMBMQI=i҈+@V5 Բpa ޟأdddElQF@DDXigyBΝc,[2/glYŅiFȑ#IIYIMMBMiPCѿ@ KKC53ZyԿpsL`l۶+27V6~>t]B1s8Vđ; [\S3_C6p㷐7Պau.nŰa NJZMFɓ$^g%IM45d+?tg{ Q{ '2gDΟx̙i3v # +?vuu.Dc>Z_458KnxGޞ:I---뮋g0xX^7aMWcb=glf~z}cut\bxxu~~[!* ovvvn۰aos-A8Ij.dҤ{yy8]ZHHé?l租~D OS=x77սzBNW^Vz?~d' M8IjA4i۾>w/Xj~"yms=ޟ;wwTTM'tAN8a %>>dxhIjNnƌ¤n޼y>>>>Vk֬1xzz<d6d;Z%%%EGmPD ƌ1cnwN]\i޴ljPIVu = є7hP_}ddCG? aKB2$ !pԄB IjBrsh&ҍ$IM!܎dɧ)G!pp2rRSB2$ !pԄB IjB!\$5ܶpwt!C-'E9ĬrDsi޽oT GR^86YrԄUAJmı>|g/Ѯ]N](Mh¹Ȑ~IRsq%I&88z5kr~zp6ݵ+>T #WU] Aֿ];n<|֭];Hb.M\܃wy,uk[b۹9lݺkUwT^|bbVQ,N_.`}L>rB[;d(IDAT? ӧiʗ'IM| HO/h. t% Fy ڇ)SnE`l8?Zj7Xx<<<4׽C ԋ˗3-;M]i_.`}hƴiII̴iB85S]֭ؽ&X ʀy##>we )sϳݕ_~E&6(˗ʂOW(ADD99yg5jpi3ƲfF;*\iaƌql6IMԄ"">?G"##أ1r< ;ǂ3Xe._dٲ9r@9MJ Ǔ㐐Ӛ%]nnԄV̙{ٶmfٳW(3dH ^CAS eu:3gcժ _sM(.%y-IOW}VM"''3gs&6Պau.nŰajUNJZMFɓ*;V>嗛cMuE*%ْԄ†̙?Ϝ9}?MӘ=}LG=a6nwLa)~>GSL&̘_KBPtt$6}Px]*Sްa}+4)Snfʔ/Wp &N _k yiƖ-6~B+7/pkN״۷P.]J->a2;!*z53gsF͛&4Gac&SIB+Nct) ##NJWa;Μ/2 5%4L4MЯ93җ Lt:l{ "222F_oܸ{{me%99Pp/;zC>EBKHHaX$?իöo~ޱ8̌YIk|mP_* GIENDB`tar-lvm/install/doc/tar-lvm-filesystem-prerequisities.png0000644002342000234200000003171213754042526023146 0ustar baxicbaxicPNG  IHDRrbKGD pHYs  tIME  : ptEXtCommentCreated with GIMPW IDATxyxSe?IZYE(tAP6epaQ舻8Q+(2jqåAqyWweǀ .X Rh9 iҦm$u+9'O<=9'!BH\Q4hѺukp x D]IC!kKnni=zx#jƍiJΊ"pC!#OܛZ]wʚ)9+(XABN)x<bpD<rrH9d$nn[Y(9+KBN)" J pC!k('S,P,P< 992$g*9+(' !'CFz:TrBĠCH.'\ZU,BN) ǣjMbpB<rrH9d JGY((cBN)"MPNB@@8P!向._()9+('0!'Ch8IIrcيpC!e'P!'CF?1V,P,PIOOCΝ{9sj+ :oڴi ̗8* 9%J :FrOMM`֭wiiia[W>cϸqXzu\%y%ljDzj5Ա2|ǭsLvgulݺ[V 5u>D:QQQ^xСCvm!ڷ?K.СC|<Ӕ׿R~#<w_9|r4N$ZE.}>\?ꪫ?~>SENDVڻcS/B楗^2ׯٱciӦ9͎;3ݻw .6mπ*w\ٳ'jD9w\앞y޽{l2ؾ};.e˖QPP?O?ofɒ%,_}X;N 8(9\yW,"IC!k%z5ugÆ { pC!#aҥ/]4)w1[`,RY {J!%Q ~`;p#n8S jRo+hz~xu7Dxƫ;!i!6J!BN%plc|DJ5uMwu~JנmIꖸ}K]"BDp<8Y]#DӰ8C*'!dV!tubX;1RuOp5?GU9q G\f|P0~.qY\MZ#^&lˀM&Tuf?X[m˖hj_/@zPc&c+[rB8]Z!`~do^j]Amj/= $phT\`p)b[lnw hownj [ѥU!!'إd f[˞v}ANkNk\J !!'.7Jα_ `5>.}Y0.ge+BH h\^Ze`SLZo|Us 9!$䄄X#9Cմ_o 1`lEڑ.q.Jȉ ך\kDh8 | iAW w 9|jJ9z0)Drf[6Ef/,ҁ}ͻ@suK"^ѥcPfogZ?E TϦE"^v?~w,"ac:Kвb`FH "<᪜jH\=`N 9!$D#0;hC!ĝ%BBNgK#nN| M#//гgOz?/B.X@_M#Dj X< }'6x8S[C8iZ,Q[ ^{ rBH%Fr$A+9;zm[ر#x<JKKԩS}FFF1WuҩS'lr?nv>Cf̘Yv-^7Qle` :(:C=T:3yX3A9$ԚZ68ZghmlwiX[ 0RcBأ.֔%9"W?[[뮻xw߿?'O&///+`ƌ|7 8믿 /oٳ; .nݺ>={6=z`ذat֭AmD6Q6x%~!eKؖs8{[8Θi??,d+]r . RXXHzzz An ^/ 8lλ[%כG)((ΣuqgϞɓ6l۷gԨQUn^<#%%%GqWaÆ1lذ_|YY}Onn.!yi[oUZ6|#L|NwwhE4|\0Xx8rSCa`7ZۙKu= 9!]ZihTǛG~~>'pL4 |_QrcCiP< LwiBu8|O K}tBcw{{p rB49XCrB6=֦b^zu][͛s'2o< ?k+e~z 5ky~!{ c!-hr)├VJ`2!C8VK)oVrBHȉF(%k_SUI?F@BN 9,j!99{J !!']Zs5Fr$HOL\F#"|Zl YgŲe*r!!'DPEΡ 3֒&D4&/_ܹs9rd5J3 u+%NPB+E!Y6T 7߿yaݻ9iժ۷o,߶mZ裏f$%%vy'կ~1C>}xT u4oޜ͛W^2>KiӦ 㭷bƌpG3p@֬Yg_~pI'QPP s0a6mTpbl pD\B#!'We)EkJuI C%H}J$!A)%%  `ذar3gwysrR/o/T>,X>_vZϟϧ~ 7^AA#GO>s饗2f|*~ꩧhѢyyycBGuSl~%bB\VnL&BG})//feӦM3MEEy `ƌcviFm#?ϻ\J۲>|ٹsYxqeL^3g5\cve&L`cV} |Su"?mMo{tߓ4yns=s=Wib?Xsnw'ZhߩS'6oތ1&08pDrא!CʢBغ3{R 9B.ɪ}/۩TfxZWo-@`5{67W#[m(K$B|7J|?;xE?\O"R<:}-JS6 kT/{ca5Ϛj_"=V7 Jr"^ nQ1!!'\LV  *rtҪWtiUɹiϒUruuA-O] U\w2!UD 9Hz SSSꍺe˖2"(x<k%ggg+9בjO"ҥK?^tr"";r"RY vqB!"$= "qZ B"B8T !!'BHȉ8GVBH PTB!!'B!!'B!!'D9!rB8UBH !!'BH јҪB 9!*rB!$䄐B!$hLtiU!E9!rJ¡"'BBN"/BBN 9!"6IR8%/6"HDV3eqhuI8 ٚK➕rBB.يq 888z= H>xSݔpik|jM+G$bA]!jV+C(9Դ WmT窫= = &5`/Khawxʫe* "'"ee++DpC<˷Rp&QEN+A`OO !uOP; hMIpd%lYR -`+qL6N|O "q3=jU@VlO #䒁k)<?we+sm"H.|O|?+OeSuI6lwauy20F+ic͟nJxwзk2S։J@ gB4wiΉז oN=AK,;:d +p.|?c"Do<`{\]$,lvÖM Kʀlˮe̠ ws e ˦!iKewDC 'aK AHm^|{h;9eQ TÿGߓZ惘 F;`w\]#F=1F3`F?pBDn疝f׫{DCƉI CV"1DL 9h`R\{_]"pMPz^]"b 7[ɴh8~ ,K|O ѠثrƉpxK vW{~JDsoouvI44ܿZBIw=p1QߑX lF̝r~yРA͎8∾+W٦M]v]z/]tUΠAb)2'|qo83?L1>y8pU?w\vݟ&''޽۷p걵n,\@"A9y?裴^11@[矟+OuU夤N}}733s_СC!ק~Zg#=GyXNj.r>e[Y~ZO|c@ff{^Dr駻iMؾ@xggggӱcGNJ38#TL\ԘX۷oKZZnZ%Oܛ"[%%%-$u붷K.$߿?a*YYYzjQM}*Br^֙v"hcVn7T$DZR(L!"nP CIH_K TYH5&BcK&D 9H4zX_UqcNsBĜs$''4&BNdk"&=r"-[=֘x91 QG!rJ#=N#;\.DH5ұDL 9EVbOkLDS?!7w"zlEc"|M#dENx(Ŗw\.Il|MĄ"@'QS?D(5D,ZȟuFBRRYيPktJĊr4T ^7OZH)8VE" Mbr} f dh,_C3EӓKځ3Չ&?" +dddT/..u4A kuIf`С9KxCGN>>_grr2~!;v |uV~pAG^oٲA:ſN:{嬴!k5+))ic{۶mL23fPZZ=4^ ==}/sX&ڥU'Z1)S=csRb5wZ۩3<,:,^z*Fr(,,s3`޼yO䐑mFFF999޽N:ѩSJ}W 9۷/5n/qo]Z:l~/"=\v{ntBΝJΝҥ w&==*rEѽ{wKQQQD g;vW^ir[6v89q</c1?]v/"vvsN 3 98;v,YYYZEt*};v,{fڵrwl2.R***XhQE.#%%yFA۶mywY|9_|E{CG}$NV\/N^xƎK^Xf ?_|,^8ŋh+7tGu׿Ju۬>"l>XSAc䤱 ߊX1?v'Oz;{WLO<w;v}'7nGuF`ܹ1 /\#FTJzqNʱKJJ ӦMh{H\VNv]wѶm=䓸JVBm ##v1mڴj;v`ҤI :UD&?eTK{c1?]t/geӦM< >c=Q'j?4333}vo^zqg{gaa 8={r^uǩ`L4,?YYYx4n޽{l?d+zRJNNNivڵknܹ3&L`̘1DA`Ag~ߎ|,V}0n8-Z***;TkQ,֯_OII ]v4Nر#6mb 9{Gr wcM(..㏯4 ǥ+Fڇ d=L vb:ȿ>\Vϟ_V?﷕M6h+駬,&S*‰d6m۶8MÍˢb}|ceՎϐ!CXd F|\v=z4'NdΝܹ'n޽;p%Lnn.,\ Ő!C^ߘ1c?~?s[hEZو#0?\_%uVxwnv]T$S5 R%Ry\+d+\4&B4N"X@BNgu!+WmOc"ŞYgU(85&BpI5R3RQogtiLbQr::vnjLHp!H BюUJNĦSr?Ƙppي ,4ɨa\!t!.x,rHY}5DyXtq 4&BFO$r:P0S sP;B 9YԐ y"!!'bJ޼ySSSkΗ_~1ޏzSSSSRRRu۷_oW_iLDv=pǼ[n B!B! DDҴIENDB`tar-lvm/install/doc/07-removal.html0000644002342000234200000000754113754042526016403 0ustar baxicbaxic Removal

Tar-LVM

Removal

Choosing the uninstallation action

You need the Tar-LVM installation archive for the installed Tar-LVM version to be able to uninstall properly. The install.sh script itself contains the list of files and directories to install or uninstall for the Tar-LVM suite of certain version.

There are two possible ways how to uninstall depending on your decision whether to keep or not to keep current Tar-LVM configuration. The purge action removes everything, whereas the remove action keeps your current configuration.

Removing everything excluding configuration

If you want to keep the configuration files, use the remove action of the installation script.

  • ./install.sh remove
Removing everything including configuration

If you want to remove all files and directories including those with configuration, use the purge action.

  • ./install.sh purge

 

 
tar-lvm/install/doc/06-restoring-the-backup.html0000644002342000234200000000326413754042526020770 0ustar baxicbaxic Restoring the backup

Tar-LVM

Restoring the backup

Restoring the backup created by the Tar-LVM suite must be performed manuallly. See the document Backing up with tar for detailed explanation and description.

 

 
tar-lvm/install/doc/01-overview.html0000644002342000234200000001627013754042526016575 0ustar baxicbaxic Overview

Tar-LVM

Overview

Introduction

The Tar-LVM suite is a set of Bash scripts that facilitates backing up Linux operating systems using GNU Tar. It is designed to perform consistent backups of the whole operating systems by creating snapshots of their filesystems either by using LVM or by remounting non-LVM filesystems read-only during the backup. The Tar-LVM tools can be thus invoked while the systems are running with minimal downtime of most services that is necessary for creating the snapshots. Because GNU tar is used as the archiving tool, the backups are done on the filesystem basis, i.e. from inside of the running systems.

Tar-LVM is able to store the result compressed archives to a remote network storage accessible just via SSH, e.g. to a NAS, to a server with a large RAID array or simply to a chosen local device if backing up a single host (e.g. a Linux workstation). Moreover, it also offers the possibility to be invoked on a physical server with the Linux native KVM virtualization and attach the backup device to each virtual machine during its backup. The backup device must be connected to the physical machine in that case.

Deployment scheme

As mentioned above, the Tar-LVM tools can be used to backup chosen Linux hosts over network to a network storage via SSH. The destination data storage can be a standalone system such as a NAS or a device connected to one of the machines (e.g. a RAID array or simply large hard disk).

The scheme is depicted on the following figure.

Deployment scheme of Tar-LVM tool

As you can see, it is possible to backup whole physical hosts with virtualization (tested with KVM), virtual hosts or separate hosts. The network backup can be run in parallel and it is possible to specify maximal number of parallel backups per each trigger point, i.e. per node which triggers the backup for a subset of hosts.

Main features
  • consistent backup of a snapshot of each operating system
  • short downtime of all services if LVM volume layout is suitable
  • consistent operating system backup even without LVM, but with long downtime period covering whole backup
  • network backup over SSH and/or local backup to any device attached to the machine or to the physical machine in case of KVM virtualization
  • backup from inside of running operating systems
  • support of any filesystem fully supported by GNU tar including POSIX ACLs (tested with ext2, ext3, ext4, xfs and btrfs)
  • support of both full and incremental backups
  • encrypted network communication
  • optionally encrypted backup device with LUKS, but no support of encrypted archives
  • parallel network backups with maximal number of backups running concurrently
  • network backups triggered from chosen number of points, e.g. centrally from one point, from several points or from each backup node
  • log output to files on the target storage and sent by mail
  • separate detection of backup errors by using the check mode with backup result analysis
  • designed to be run automatically from cron

 

 
tar-lvm/install/doc/03-installation.html0000644002342000234200000001342713754042526017433 0ustar baxicbaxic Installation

Tar-LVM

Installation

Choosing the installation mode

If you've prepared your system for the backup, you can proceed with the installation. You must install the Tar-LVM tools on each host separately and there are two installation modes denoted as all and one.

The all mode must be used on the machine or host where the tar-lvm-all script is invoked. That's either the machine to which the backup device is connected if it is accessed directly (i.e. not via SSH) or the host which triggers the network backup centrally for a set or subset of hosts via tar-lvm-all.

Using the one installation mode is sufficient on all other physical or virtual hosts that are backing up via SSH and on the KVM virtual machines with direct access to the backup device connected to the physical machine.

Information to both modes

The installation script installs the binaries into the /usr/local/sbin directory, configuration files into /usr/local/etc, documentation into /usr/local/share/doc and creates the required directory structure under /var/local. Whereas older versions of the binaries are always overwritten, the original configuration files are kept untouched and the installed configuration files got the .new suffix if the corresponding configuration files already exist.

Keep the installation archive just for case of future uninstallation. The install.sh script itself contains the list of files and directories to install or uninstall for the Tar-LVM suite of certain version.

Installation of the whole suite - the all mode

The installation procedure is very simple. It's sufficient to download and extract the Tar-LVM installation archive and then run the included script install.sh.

To install the whole suite, pass the all argument to the installation script.

  • ./install.sh install all
Installation for a single host or machine - the one mode

To install just the scripts for a single host or machine, pass the one argument to the installation script.

  • ./install.sh install one

 

 
tar-lvm/install/doc/00-backing-up-with-tar.html0000644002342000234200000005464113754042526020507 0ustar baxicbaxic Backing up with tar

System backup

Backing up with tar

Introduction

This article explains how to use GNU tar to create a Linux based operating system backup and how to restore it later if needed. It assumes that the filesystems to backup are the traditional Linux ext2 based filesystems, i.e. of type ext2, ext3 or ext4, or widely used xfs or relatively new btrfs filesystems. It was tested that these filesystems can be fully restored if proper arguments are given as explained later. The described steps should work for other Linux filesystems as well if they're able to store all standard Linux file types and attributes, but that wasn't confirmed at the time of this writing. However, it was successfully tested that GNU tar is able to backup one filesystem of the aforementioned type and restore it to another type. Later versions of GNU tar (e.g. version 1.27.1) support even POSIX ACLs and can be used to perform not just full, but incremental backups as well.

GNU tar capabilities

GNU tar is capable of archiving all Linux file types except unix domain sockets. However, the file of this type normally exists only if the process that created it is running unless the process exits abnormally. Hence, this isn't an issue. All other file types including regular files, directories, symbolic links, named pipes and character and block devices are stored. All important information about these file types is contained in the archive: filenames, permissions, owner, group, modification time, symbolic link target and major and minor device numbers. Hard links are stored and extracted properly too if they're put into the same archive. GNU tar can also handle sparse files efficiently, but only if it obtains the --sparse option during the backup. Later GNU tar versions are able to backup and restore POSIX ACLs if the --acls argument is given to tar during both creating and restoring the archive. However, the special lost+found directory in the root directory of an ext2 based filesystem is treated as a usual directory and should be therefore excluded from the backup by using the --exclude option.

Furthermore, GNU tar can create incremental backups if it is called with the -g switch. It uses ctime (time of last modification of file status information) to determine whether the file or its status has changed. This is a second best choice after using hashes because if the file contents changes, the mtime (modification time) and therefore also the ctime change. However, if the file is only accessed and atime (access time) is modified, ctime remains unaltered. If only the file status changes (permissions, owner, group etc.), ctime is also updated. The only drawback of this method is that it is necessary to maintain correct system time. It is not possible to modify ctime arbitrarily by using some command, but if the system time changes, later modifications may occur as older ones and the incremental backup will unfortunately not contain such files. You should also take into account that the -g switch requires an argument denoting the path to so called snapshot archive. The snapshot archive contains additional information that is necessary to determine whether a file should be included into the incremental archive or not. More precisely, the snapshot archive contains the list of all files and their ctimes.

Creating full backup archives

If you want to create consistent operating system backup, you need to backup a snapshot of all partitions where the system is installed. This can be accomplished either by shutting down the system and backing it up (e.g. from a live CD) or by stopping all processes that are writing to the filesystems to backup, remounting them read-only and then performing a consistent backup.

The latter approach doesn't require to stop the system. Moreover, it can be further improved by using a technology that is able to create filesystem snapshots at a particular time. LVM (Logical Volume Management) can be used for this purpose if you have enough free space in the volume group containing logical volumes with data. The writing processes can be then stopped and the filesystems remounted read-only just for the moment of creating snapshots. The system can continue running after that and the backup can be performed on the snapshots instead of on the running system.

Let's now look at the concrete commands that can be used to backup an operating system snapshot. Let's remount all filesystems read-only at first.

  • mount -o remount,ro /boot
  • mount -o remount,ro /
    ...
  • mount -o remount,ro /home

The backup archives of the snapshot can be then created as follows:

  • tar -cvvzf boot.0.tgz -g boot.0.snar -C /boot --one-file-system --sparse --acls --exclude ./lost+found .
  • tar -cvvzf rootfs.0.tgz -g rootfs.0.snar -C / --one-file-system --sparse --acls --exclude ./lost+found .
    ...
  • tar -cvvzf home.0.tgz -g home.0.snar -C /home --one-file-system --sparse --acls --exclude ./lost+found .

The aforementioned --sparse and --acls options ensure that sparse files are stored efficiently as sparse and that POSIX ACLs are included into the archives as well. The --one-file-system option instructs tar not to archive files from other filesystems. And the --exclude option is self-explanatory. Bare in mind that the special lost+found directory of an ext2 based filesystem should be always excluded from the backup because it is restored as a usual directory. The -g option is not necessary. However, you must pass it to tar in order to create subsequent incremental backup(s) later. The -g switch defines the filename of so called snapshot archive with additional metadata to identify files whose attributes or contents changed since the preceding backup.

Creating incremental backup archives

Creating an incremental backup is analogous to creating the foregoing full backup. An operating system snapshot should be made in the same way as in the case of the full backup, e.g. by invoking:

  • mount -o remount,ro /boot
  • mount -o remount,ro /
    ...
  • mount -o remount,ro /home

Archiving the filesystems is also similar. The snapshot archive referenced by the -g switch is used to identify and backup modified files. It is also updated so that another incremental backup based on the incremental backup being created can be performed later. Hence, let's copy all snapshot archives at first to keep them for each incremental backup level.

  • cp boot.0.snar boot.1.snar
  • cp rootfs.0.snar rootfs.1.snar
    ...
  • cp home.0.snar home.1.snar

The backup can be done after that in the same way as the full backup. Just the archive names are adjusted to refer to the new backup level.

  • tar -cvvzf boot.1.tgz -g boot.1.snar -C /boot --one-file-system --sparse --acls --exclude ./lost+found .
  • tar -cvvzf rootfs.1.tgz -g rootfs.1.snar -C / --one-file-system --sparse --acls --exclude ./lost+found .
    ...
  • tar -cvvzf home.1.tgz -g home.1.snar -C /home --one-file-system --sparse --acls --exclude ./lost+found .

Subsequent incremental backup archives are generated by the same commands. Just the backup level in the archive names must be adjusted again.

Restoring the backups

Let's assume that you have your disk prepared for data extraction, i.e. that it contains valid partition table and that its partitions are formatted to ext2 based or other supported Linux filesystems, ideally the same as in the original system because you don't have to modify configuration of the target system in that case - first of all the file /etc/fstab. The swap partition should be formatted as swap space by mkswap.

Then mount each empty filesystem and extract the archives to its root folder. Firstly the full backup archive and then all incremental backups in the order in which they were created. The following example assumes that the boot filesystem is located at the first primary partition /dev/sda1 and that other filesystems exist on top of LVM group mg.

  • mount -o acl /dev/mg/rootfs /mnt/restore
  • tar -xvvzf rootfs.0.tgz -g /dev/null -C /mnt/restore/ --numeric-owner --acls
  • tar -xvvzf rootfs.1.tgz -g /dev/null -C /mnt/restore/ --numeric-owner --acls
    ...
  • mount -o acl /dev/sda1 /mnt/restore/boot
  • tar -xvvzf boot.0.tgz -g /dev/null -C /mnt/restore/boot --numeric-owner --acls
  • tar -xvvzf boot.1.tgz -g /dev/null -C /mnt/restore/boot --numeric-owner --acls
    ...
    ...
  • mount -o acl /dev/mg/home /mnt/restore/home
  • tar -xvvzf home.0.tgz -g /dev/null -C /mnt/restore/home --numeric-owner --acls
  • tar -xvvzf home.1.tgz -g /dev/null -C /mnt/restore/home --numeric-owner --acls
    ...

The --numeric-owner argument is essential not to restore file owner and group names from the archive, but just their UIDs and GIDs. This becomes important when extracting using different operating system than the final one because tar doesn't change file UIDs and GIDs depending on matching usernames on the running system. The --acls option can be left out if ACLs aren't used on the restored filesystems. It doesn't conflict with the --numeric-owner option if it is used just during restoration.

The disk now contains all data, but the system is not able to boot yet.

Installing the boot loader

This step varies depending on the used Linux distribution, boot loader and disk layout. The following description applies to Debian which is using the GRUB boot loader and has its filesystems on top of LVM with the only exception of the boot directory that lays on the first primary partition /dev/sda1. However, it can be customized for other cases as well by using other commands, boot loader (e.g. lilo) or devices.

The basic idea behind the procedure is simple. Boot from a live CD, mount all filesystems into the target directory tree and run chroot to see just this tree. Then update filesystem UUIDs in some configuration files (or possibly other references to the filesystems if you changed their number, types or names), update the initial ramdisks for all kernel versions and configure and install the boot loader itself.

The filesystems were already mounted to /mnt/restore during archive extraction. However, some filesystems generated by the running kernel or daemons need to be mounted yet.

  • mount -t proc proc /mnt/restore/proc
  • mount -t sysfs sysfs /mnt/restore/sys
  • mount -t udev devtmpfs /mnt/restore/dev

The root directory of the file/directory tree can be changed after that.

  • chroot /mnt/restore

You shouldn't forget to check /etc/fstab for the filesystem references if they are correct. And if you changed the filesystem types, you must update them as well. This example assumes that all filesystems were recreated in the same way as in the original system and thus just the UUIDs had to be updated, e.g.

  • UUID=... /boot ext3 defaults,errors=remount-ro,nodev,nosuid,noexec 0 2

Filesystem UUID can be found out by using the blkid command, e.g.:

  • blkid /dev/sda1

It's recommended to check if the initrd configuration doesn't refer to incorrect filesystem as well. It needs to refer to the swap device for the purpose of resuming from hibernation. This can be done in the file /etc/initramfs-tools/conf.d/resume in Debian:

  • RESUME=/dev/mg/swap

Finally, it should be sufficient to update initrd and GRUB.

  • update-initramfs -k all -u
  • grub-install
  • update-grub

And try to reboot.

Summary

It's possible to use GNU tar to backup and restore whole Linux operating system. The backup can be done while the system is running, but you must ensure that you're backing up a consistent snapshot of the operating system.

The described method should work for any Linux distribution because the GNU implementation of the standard tar command should be part of it. Even if it is not, you could backup from a live CD distribution from time to time. GNU tar should be able to store all permanent Linux file types and important attributes and the procedure should thus work for most Linux filesystems. This method was fully tested only on ext2 based filesystems and partially on xfs and btrfs and it was used to restore several different installations of Debian squeeze, wheezy and jessie and also of CentOS 7. No problem occurred after the whole system was restored, all permissions and all important attributes were maintained as explained above.

 

 
tar-lvm/install/doc/04-configuration.html0000644002342000234200000012104013754042526017571 0ustar baxicbaxic Configuration

Tar-LVM

Configuration

Basic configuration on any host to backup
Stopping and starting processes: ssd-backup

If you want to create a snapshot of all backed up filesystems together and not of each filesystem separately, you need to stop all processes that write to the backed up locations, remount filesystems temporarily read-only and then create snapshots. After the snapshots are created, the filesystems can be remounted back read-write and the processes can be started again. This task of stopping and starting daemons or processes is performed by the ssd-backup script that is part of the Tar-LVM suite.

Let's now look at the ssd-backup configuration. This script is configured by default using the file /usr/local/etc/ssd-backup.conf.

  • ### disable sysv (System V) or systemd (Systemd) init scripts support,
  • ### just comment for the default behaviour, i.e. sysv/systemd support
  • # format: nosysv ("true"|"false")
  • # format: nosystemd ("true"|"false")
  • #nosysv "true"
  • #nosystemd "true"
  • ### sysv or systemd services to stop or start depending on the mode
  • # format: stopstart sysv|systemd <service> [[pidfile=...][,][psname=...]]
  • stopstart systemd sssd.service
  • stopstart systemd cron.service
  • stopstart systemd postfix.service
  • stopstart systemd slapd.service
  • stopstart systemd denyhosts.service
  • stopstart sysv rsyslog
  • stopstart systemd dbus.service
  • ### extended regular expressions specifying the names of processes
  • ### to kill in the stop mode (usually not services)
  • # format: kill <regexp>
  • #kill "^console-kit-dae"
  • ### real user names whose processes shouldn't be killed (should survive)
  • ### when the -u option is used, i.e. when all non-root user processes
  • ### should be killed (root is always included and doesn't have
  • ### to be listed)
  • # format: survruser <user>
  • survruser message+
  • survruser ntp
  • ### commands to run at the end of the stop mode
  • # format: stopcomm <command> <arg1> ... <argN>
  • #stopcomm echo "SSD stopped..."
  • ### commands to run at the beginning of the start mode
  • # format: startcomm <command> <arg1> ... <argN>
  • #startcomm echo "SSD starting..."

Supported directives are concisely described directly in the configuration file.

The nosysv or nosystemd directives can disable the System V or Systemd support and can thus allow usage of this script even if no service or systemctl binary is found.

The stopstart directives define all services to be stopped in the ssd-backup stop mode or started in its start mode. The services are stopped in the specified order if they're running and started in the reverse order depending on their initial status. They also allow to specify an optional PID file and/or process name to identify running services if the operating system itself doesn't provide this information for certain service.

The kill directive allows to kill certain processes based on their names. And the survruser entries allow to specify all real usernames whose processes shouldn't be killed if the -u argument is used. This argument instructs ssd-backup to kill all user processes, i.e. all non-root processes that are not excluded by survruser.

The stopcomm and startcomm directives can be used to execute arbitrary commands at the end of the stop mode or at the beginning of the start mode. They can be therefore used for any non-standard operation, e.g. remounting filesystems read-only or read-write, cleanup of some locations, sending a message to users or anything else that's scriptable.

See the ssd-status help for more information and complete list of its arguments.

  • ssd-status -h

It's wise to confirm that ssd-backup is configured properly before continuing. Simply by invoking a command sequence that's similar to the following one. But be prepared for short downtime of your services.

  • ssd-backup -u -v stop
  • mount -o remount,ro /var
  • mount -o remount,ro /
  • ...
  • mount -o remount,rw /
  • mount -o remount,rw /var
  • ssd-backup -u -v start

All the ssd-backup and mount commands should succeed, of course, if the configuration is correct. If you need to identify the processes that are using files on certain filesystem, try the lsof or fuser commands.

Creating and removing snapshots, backing up: tar-lvm

The snapshot of the writable filesystems backed up from inside of a running operating system can be created by remounting the filesystems read-only, creating LVM snapshots and remounting the filesystems back to their original state, mostly often back read-write. Or by remounting the writable filesystems read-only during the whole backup process. However, the downtime of your services is much longer in the latter case.

All these operations are performed by the tar-lvm script. Its pre mode remounts the filesystems read-only, creates LVM snapshots are remounts LVM filesystems back read-write. Its run mode creates the tar backups and the post mode remounts the non-LVM filesystems back to their initial state.

The tar-lvm script is configured in the file /usr/local/etc/tar-lvm/tar-lvm.conf which has the following syntax.

  • ### suffix of the LVM snapshot names appended to the name of the origin
  • # format: lvsnapsuffix <suffix>
  • lvsnapsuffix ".tar-lvm"
  • ### disables ACLs support if set (necessary for older GNU/tar versions),
  • ### just comment for the default behaviour, i.e. ACLs support
  • # format: noacls ("true"|"false")
  • #noacls "true"
  • ### filesystems not on LVM
  • # format: fs <name> (<device-path>|UUID=<uuid>) [<path-to-exclude> ...]
  • fs "boot" "UUID=621393c4-1827-4b6a-b053-1f249a844626"
  • ### filesystems on top of LVM
  • # format: lv <name> <group> <snapshot-size>% [<path-to-exclude> ...]
  • lv "rootfs" "mg-baxic-prod" "20%"
  • lv "usr" "mg-baxic-prod" "20%"
  • lv "var" "mg-baxic-prod" "80%"
  • lv "srv" "mg-baxic-prod" "80%"
  • # don't backup tmp because it's only temporary location and its contents is
  • # often deleted on system startup on some systems
  • #lv "tmp" "mg-baxic-prod" "80%"
  • lv "vartmp" "mg-baxic-prod" "80%"
  • lv "varmail" "mg-baxic-prod" "80%"
  • # don't backup varlock because it's only temporary location and its contents
  • # is often deleted on system startup on some systems, moreover, varlock cannot
  • # be remounted read-only because LVM creates file locks in it during snapshot
  • # creation
  • #lv "varlock" "mg-baxic-prod" "80%"
  • lv "home" "mg-baxic-prod" "20%" "./baxic/data"

Supported directives are concisely described directly in the configuration file.

The lvsnapsuffix entry defines the suffix that is appended to the names of the LVM volumes to get the corresponding LVM snapshot volume names.

The noacls entry can disable support for POSIX ACLs. This becomes important on older systems whose GNU tar version doesn't support the --acls option yet.

All other directives, i.e. the fs and lv items specify the filesystems to backup. The fs directive refers to non-LVM devices with filesystems that are remounted read-only during the whole backup process. The device can be specified either by device path or by its UUID.

The lv lines refer to LVM logical volumes by their names and volume groups they belong to. Because new snapshot logical volume is created for each logical volume to backup in the tar-lvm pre mode, its size must be specified as a certain number of percents of its origin.

Furhtermode, both fs and lv entries can contain more optional arguments that list directories or files that should be excluded from the backup. See the lv entry for the home filesystem above for an example. The path should always start with a dot and is relative to the filesystem root directory.

To confirm the validity of new configuration, the following command sequence should succeed. Be prepared for short downtime of your services again if you're using LVM or for longer downtime comprising the whole backup process if your read-write filesystems are not on LVM.

  • mkdir /tmp/tar-lvm-test
  • ssd-backup -u -v stop
  • tar-lvm -v pre
  • tar-lvm -v -f run 0 /tmp/tar-lvm-test
  • tar-lvm -v post
  • ssd-backup -u -v start
Automating the backup
Wrapper script for one host backup: tar-lvm-one

The tar-lvm-one wrapper script must be configured to simplify the automation of a backup of a specific host. This script invokes the commands described and configured earlier and also mounts and unmounts the backup device in between.

This wrapper script can be configured either on each host separately in local configuration files or in a shared configuration file located on one host denoted as allhost in the local configuration. The syntax of both files is identical and the directives specified in the local configuration file override the shared directives. The shared configuration doesn't have to be used. This is the case if no allhost directive is present in the local file. However, the local configuration file cannot be omitted and must specify all needed directives locally or at least the allhost directive pointing to the host with shared configuration.

Let's now look at an example configuration shared among more hosts from one allhost. The local configuration must then contain the allhost directive on each host. All other entries are optional and become useful only if the shared configuration should be overriden by something more specific on certain host.

The local configuration file /usr/local/etc/tar-lvm/tar-lvm-one.local.conf must contain at least the following part in our example.

  • # hostname of the machine that contains the shared configuration file
  • # (and usually runs the tar-lvm-all script if used - hence the name),
  • # the machine must be accessible by ssh public key authentication, i.e.
  • # without using a password, comment the allhost line if no shared configuration
  • # file should be copied to localhost and used
  • # format: allhost <hostname>[:<hostname_fqdn>]
  • allhost "baxic-pm:baxic-kvm-1.domain.org"

In this case, the shared configuration file /usr/local/etc/tar-lvm/tar-lvm-one.shared.conf must define all other directives.

  • # disable sshfs backup filesystem support, just comment for the default
  • # behaviour, i.e. both block device and sshfs support
  • # format: nosshfs ("true"|"false")
  • #nosshfs "true"
  • # backup device, either local device or remote sshfs filesystem, the local
  • # device is specified by name (not by whole path) or by UUID and the device
  • # must be located in the /dev directory, the remote sshfs filesystem is
  • # specified by the host and optional user and path
  • # format: dev (<name>|UUID=<uuid>|[<user>@]<host>:[<path>])
  • #dev "UUID=f3d286d1-aa4a-6f32-a367-ab93e72cbfa8"
  • dev "root@baxic-nas.domain.org:/backup-data"
  • # device mapper name used by cryptsetup if the backup device is local and
  • # if it is encrypted by LUKS, comment the dm line if the backup device isn't
  • # encrypted
  • # format: dm <dmname>
  • dm "backup"
  • # backup filesystem mount point
  • # format: mntdir <bkpfsmntdir>
  • mntdir "/mnt/backup"
  • # directory on the backup filesystem containing the backup directory tree
  • # format: rootdir <bkprootdir>
  • rootdir "tar-lvm"
  • # defer ssd-backup start after the whole backup is complete or comment
  • # for the default behaviour, i.e. ssd-backup start after tar-lvm pre
  • # (ssd-backup stop, tar-lvm pre, ssd-backup start, tar-lvm run, tar-lvm post)
  • # format: deferssdstart ("true"|"false")
  • #deferssdstart "true"

The nosshfs entry supresses usage of the remote SSHFS backup filesystem and also the check that the sshfs and fusermount binaries are present. It should be therefore used on systems without these binaries installed.

As mentioned earlier, the backup device can be either a local device defined by its name or UUID or a remote SSHFS filesystem. All possibilities are specified by the dev line depending on its syntax.

If the backup filesystem is located at a local device and if it is encrypted by LUKS, the dm line is required. It instructs tar-lvm-one to ask for password and decrypt the device before it is mounted.

The mntdir entry is a mount point of the backup filesystem, i.e. a directory to which the backup filesystem is mounted during the backup.

And the rootdir line specifies a path to an existing directory on the backup filesystem where the backups and logs should be stored.

There's one more optional entry in the configuration file and that's the deferssdstart flag. If it is set to true, it instructs the script to defer ssd-backup start as the last operation. The default behaviour differs, ssd-backup start is invoked immediately after the LVM snapshots are created, i.e. before the tar backup itself. This becomes handy if some read-write filesystem is not located on LVM and if no snapshot can be thus created.

The tar-lvm-one configuration and backup can be tested as follows:

  • tar-lvm-one -f all 0
Choosing the triggering model: centralized or distributed

There're two ways how to trigger the backups on all hosts: centralized or distributed. The first centralized way must be used if you need to backup whole physical machine with separate backups of its KVM virtual machines to a device that is connected directly to the physical machine. The reason is that it needs to attach and detach the backup device to the virtuals. However, if you want to backup just via SSH with the exception of the machine the device is connected to, you can choose whether to use the distributed or centralized triggering model.

The main difference between both models even if using just a SSHFS storage is that the centralized way is managed by the tar-lvm-all script which controls the whole process, the backups are triggered sequentially and there's a maximal number of parallel backups that can run at once. The distributed way is managed and controlled on each host separately, backups are usually triggered from the Cron scheduler at a specific time and they all run independently of each other. Both models use the tar-lvm-one wrapper script. This script is invoked by tar-lvm-all in the former case, i.e. in the centralized model, and directly from Cron in the latter case, i.e. in the distributed model.

In fact, both models can be combined and used at once, but there's one significant limitation. If tar-lvm-all attaches the backup device to some of its virtuals that store the backups directly to this device, the distributed model shouldn't be mixed up with the centralized one unless it uses different backup device. The backup device can be mounted just by one machine at a time if using the direct access to avoid data corruption.

Centralized triggering model: tar-lvm-all

If you chose the centralized model of triggering the backups, let's proceed with configuration of the tar-lvm-all script. This script must run on the physical machine the backup device is connected to if this machine or its KVM virtuals do not want to use SSHFS, but the more efficient direct access method to access the backup device. Otherwise if all hosts to backup store their backups remotely using SSHFS, it can be run on any host - even on a host that's not going to be backed up.

The tar-lvm-all configuration file is located at /usr/local/etc/tar-lvm/tar-lvm-all.conf and it looks as follows.

  • # backup device on the physical machine if at least one backup to local
  • # device should be performed (not needed for SSHFS), the device is
  • # specified by name (not by whole path) or by UUID and it must be
  • # be located in the /dev directory, it can be either whole disk,
  • # partition or volume etc.
  • # format: pmdev (<name>|UUID=<uuid>)
  • pmdev "UUID=0da345bc"
  • # backup device to create on the virtual machines if at least one backup
  • # to local device should be performed (not needed for SSHFS), the device
  • # is specified by name (not by whole path)
  • # format: vmdev <name>
  • vmdev "vdb"
  • # set to "true" to enable password prompt, the prompt is needed if the backup
  • # device is encrypted and a password should be therefore passed to tar-lvm-one
  • # (for each virtual machine), just comment if not needed
  • # format: passprompt ("true"|"false")
  • passprompt "true"
  • # set to "true" if you want to backup the physical machine as well, otherwise
  • # comment or set to "false", the machine must be accessible as localhost by ssh
  • # public key authentication, i.e. without using a password
  • # format: pmbackup ("true"|"false")
  • pmbackup "true"
  • # names of the virtual machines to backup, the names are hostnames as well,
  • # but you can specify different hostname appended to the machine name
  • # behind a colon, e.g. as "machine:host.domain.org", the machines must
  • # be accessible by ssh public key authentication, i.e. without using
  • # a password
  • # format: vm <hostname>[:<hostname_fqdn>]
  • vm "baxic-prod"
  • vm "baxic-prod-old"
  • # hosts to backup using sshfs, the names are hostnames as well, but
  • # you can specify different hostname appended to the host name
  • # behind a colon, e.g. as "host:host.domain.org", hosts must be accessible
  • # by ssh public key authentication, i.e. without using a password
  • # format: host <hostname>[:<hostname_fqdn>]
  • host "baxic-test-1"
  • host "baxic-test-2"
  • host "baxic-test-3"
  • # number of hosts to backup in parallel, remaining hosts must wait until
  • # the preceding backups finish, this setting doesn't apply to vm's (i.e.
  • # virtual machines) that are always backed up one by one
  • # format: parhostnum <number>
  • parhostnum 2
  • # notification email addresses
  • # format: mailto <email>
  • mailto "backup@domain.org"
  • # SMTP server if neither mail nor mailx (i.e. local MTA) should be used
  • # and SMTP should be used directly, simply comment if local MTA should be
  • # used instead
  • # format: smtp[-tls|s]://[USER:PASS@]SERVER[:PORT]]
  • #smtpserver "smtp-tls://user@gmail.com:secret@smtp.googlemail.com"
  • # notification email sender if smtpserver is specified
  • # format: mailfrom <email>
  • mailfrom "user@gmail.com"

If at least one KVM virtual machine is backed up to a local device connected to the physical machine and not to a remote SSHFS filesystem, both pmdev and vmdev entries must be defined. The pmdev entry defines the backup device on the physical machine that should be connected as the vmdev device to the virtual machines. However, both entries are required only if at least one vm entry is present.

Another optional entry is the passprompt flag. It instructs tar-lvm-all to ask for password that should be passed to tar-lvm-one if the backup device is encrypted.

The pmbackup flag determines whether to backup the physical machine (i.e. the machine the script runs at) or not. If it is set to true, root@localhost must be accessible from root@localhost by SSH public key authentication, because the backup is performed in the same way as in the case of backing up other host. Just the backup device doesn't have to be attached.

The most important entries are the vm and host directives that specify the nodes to backup. More specifically, the virtual machines with direct access to the backup device and hosts to backup remotely via SSH. Their names are especially important by the vm lines because they are equal to the names of the KVM virtual machines. They can be also used to refer to DNS hostnames if they belong to the resolver search domain, but if the names are not resolvable, an optional colon can be used to append FQDN, IP address or another resolvable hostname entry.

There's one more entry that influences the way how the remote SSHFS backups are triggered - the parhostnum directive. It specifies maximal number of hosts to backup in parallel and if this number of hosts being backed up is reached, all reamining hosts must wait.

The remaining lines configure email notification and they're mostly self-explanatory. The only important thing to take into account that may not be obvious is the fact that if no smtpserver directive is specified, then local MTA is used to deliver emails. If it is specified, emails are delivered directly via SMTP to the specified mail server. However, that's not the preferred solution. If the delivery fails, there's neither a way how to inform about the error nor any later repeated delivery attempt. The error output can get lost easily.

If you want to test the backup for all machines and hosts, simply invoke tar-lvm-all on the host that manages the backup process centrally.

  • tar-lvm-all -v -f all 0
Distributed triggering model: tar-lvm-one

If you chose the distributed model of triggering the backups, there's no need to configure any other tool, because tar-lvm-one is used for this purpose and this script was already configured earlier.

 

 
tar-lvm/install/doc/05-triggering-the-backup.html0000644002342000234200000003407013754042526021113 0ustar baxicbaxic Triggering the backup

Tar-LVM

Triggering the backup

Manual backup

Whether you have configured Tar-LVM just basically for manual backup or more thoroughly for automated backup, you can always run the backup manually from the command line. Both ssd-backup and tar-lvm scripts are installed and must be configured irrespective of the model used.

If all your filesystems that must stay read-write during normal system operation are using LVM volumes and if the tar-lvm script is configured to create and backup their LVM snapshots, full manual backup can be invoked by using the following command sequence.

  • ssd-backup -u -v stop
  • tar-lvm -v pre
  • ssd-backup -u -v start
  • tar-lvm -v run 0 /path/to/backup/dir
  • tar-lvm -v post

The downtime of all services to stop is really short, because it covers only the period that is necessary for creating LVM snapshots. After the snapshots are created, the system can operate normally because the contents of the snaphots is backed up.

If the read-write filesystems are not on LVM, their backup can be performed as well. The only difference is the duration of the downtime period that must cover whole backup process in order to create consistent backup of whole operating system.

  • ssd-backup -u -v stop
  • tar-lvm -v pre
  • tar-lvm -v run 0 /path/to/backup/dir
  • tar-lvm -v post
  • ssd-backup -u -v start

Consecutive incremental backups are obtained in the same way, just the level argument of the tar-lvm command in the run mode must be increased. E.g. a level 1 incremental backup can be obtained as follows.

  • ...
  • tar-lvm -v run 1 /path/to/backup/dir
  • ...
Automated backup
Centralized triggering model

A compulsory prerequisity for the centralized triggering model is to have the tar-lvm-all script installed and configured at the host triggering and managing the backups. Its usage is then simple, just don't forget that SSH public key authentication must be set up as described earlier.

Let's edit the configuration of the Cron scheduler at the triggering host.

  • crontab -e

It could look as follows to run the backups at midnight. Full backup is done on Sunday and the incremental ones on all other week days.

  • SHELL=/bin/bash
  • MAILTO=backup@domain.org
  • PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
  • ...
  • 0 0 * * * tar-lvm-all all "$(date '+%w')"

The backup results can be checked automatically as well and mailed by Cron. This must be configured at the triggering host again, i.e. at the host where tar-lvm-all runs.

  • crontab -e
  • SHELL=/bin/bash
  • MAILTO=backup@domain.org
  • PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
  • ...
  • 0 6 * * * tar-lvm-all check "$(date '+%w')"
Distributed triggering model

As it was already mentioned, the distributed triggering model doesn't require to have the tar-lvm-all script installed or configured, tar-lvm-one is sufficient. This also means that the direct access method of the backup storage shouldn't be used and that the backup storage should be always accessed remotely using the SSHFS filesystem. The reason is that this method allows concurrent access to the backup device from more hosts.

If you want to run the backup of certain host at midnight with full backup on Sunday and incremental backups on all other week days, simply configure Cron as follows at the backup host, i.e. at the host being backed up.

  • crontab -e
  • SHELL=/bin/bash
  • MAILTO=backup@domain.org
  • PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
  • ...
  • 0 0 * * * tar-lvm-one all "$(date '+%w')" && echo 'tar-lvm-one result => ok' || echo 'tar-lvm-one result => failure'

The backup results can be checked automatically again and mailed by Cron if the tar-lvm-one script in the check mode produces some error output. This must be configured at the backup host again.

  • crontab -e
  • SHELL=/bin/bash
  • MAILTO=backup@domain.org
  • PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
  • ...
  • 0 6 * * * tar-lvm-one check "$(date '+%w')"
Rotating the backups and logs

It's wise not to use the -f argument to delete existing archives and logs of the same or higher backup level, but save the target backup directory at the backup storage before each new full backup is created.

The simplest solution could be configured in Cron at the backup storage. If the full backup is created on Sunday and incremental backups on another week days, let's save just the backups for the last week and delete everything else.

  • crontab -e
  • SHELL=/bin/bash
  • MAILTO=backup@domain.org
  • PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
  • ...
  • 50 23 * * 6 cd /path/to/backup/storage && rm -fr tar-lvm.old && mv tar-lvm tar-lvm.old

There are many ways how to achieve backup rotation depending on your needs. Another example storing all backups for the whole year could be configured using Cron again at the backup storage.

  • crontab -e
  • SHELL=/bin/bash
  • MAILTO=backup@domain.org
  • PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin p-
  • ...
  • 50 23 * * 6 cd /path/to/backup/storage && rm -fr "tar-lvm.$(date '+%W')" && mv tar-lvm "tar-lvm.$(date '+%W')"

 

 
tar-lvm/install/doc/02-preparing-for-the-backup.html0000644002342000234200000005325113754042526021524 0ustar baxicbaxic Preparing for the backup

Tar-LVM

Preparing for the backup

Choosing destination backup storage

Tar-LVM suite is able to store the backups either via SSH or to any local device connected to one of the machines. Moreover, the device can be connected to one of the physical machines with KVM virtualization and attached to its virtual machines during their backup. Both possibilities can be combined together.

The scheme is depicted on the following figure.

Deployment scheme of Tar-LVM tool

The created archives and logs are thus written directly to the mounted backup device or to the SSHFS network filesystem using the SFTP protocol. However, bare in mind that the direct access method for storing the backups can be used just for a single physical machine and its KVM virtual machines, all other hosts (either physical or virtual) must use SSHFS if they want to store the archives and logs to the same storage.

This means that the backup destination can be a network storage accessible via SSH (e.g. a NAS) or any device directly connected to one of the physical machines with Linux (such a a large RAID array or simply a hard drive).

There's one more viewpoint that should be taken into account when choosing the backup storage. If it is intended to be accessed directly from one physical machine and its KVM virtual machines, then the Tar-LVM suite must be triggered from that physical machine to be able to attach the backup device to each machine sequentially and not concurrently. All other hosts using the SSH access method can be then backed up in parallel and the backup process may be triggered from the physical machine as well, from each backed-up host or from any other host.

Backup host filesystem prereguisities

If you want to achieve minimal downtime of your services and still be able to create consistent backup of a snapshot of whole operating system, most of its filesystems must be located on LVM devices with suitable layout. The reason is simple. If a snapshot of a running system is to be created, all filesystems must be remounted read-only during the snapshot creation and those that are not located on LVM and thus are unable to create snapshots, must stay remounted read-only during the whole backup process.

This idea is expressed on the following figure.

Scheme of Tar-LVM filesystem prerequisities

As you can see and as it was mentioned above, all fileystems don't have to be necessarily created on LVM volumes. Those of them that are mounted read-only during normal operation (i.e. most of the time - except maintenance) don't need to meet this condition.

Depending on your filesystem layout, those are usually filesystems mounted to the /boot or /usr directories and even the root filesystem / itself can stay read-only most of the time if there are separate partitions or volumes for writable locations like /var, /srv, /opt etc. The /dev or /run (/var/run) directories are nowadays usually located in memory.

However, there's an important condition if the backups should be triggered automatically by the Cron scheduler. In that case, the /tmp directory must stay writable during archiving. This isn't luckily any issue, because the temporary files are usually deleted when the operating system starts. And that's the case after restoring whole system as well. There's therefore no need to backup the /tmp or /var/tmp filesystems.

Let's look at two examples of the filesystem layout. The first layout contains many separate filesystems and most of them are located on LVM. Even those that can be mounted read-only during normal operation such as the /usr or / filesystems. The LVM read-only filesystems can be safely remounted read-write during the backup after their snapshots are created.

mountpointLVMread-only
/LVMro
/boot-ro
/usrLVMro
/varLVMrw
/tmpLVMrw
/var/tmpLVMrw
/var/mailLVMrw
/srvLVMrw
/optLVMrw
/homeLVMrw

On the contrary, the second example layout contains as few filesystems as possible for the Tar-LVM backup to operate properly and none of them are on LVM.

mountpointLVMread-only
/-rw
/boot-ro
/tmp-rw

The advantage of the first layout is obviously the fact that the downtime of services provided by the backed-up host is minimal. It's necessary just to stop most services, remount filesystems read-only, create snapshots, remount filesystems back read-write and the services may be started again after that because the read-only filesystems and frozen snapshots are being backed-up. The second layout enforces remounting the root / filesystem read-only during the whole backup which necessitates shutting down most services during the whole period.

The topic of partitioning the system is behind the scope of this document. If you want to know more about the reasons of creating several separate partitions, see the document Securing Debian Manual, especially its parts Partitioning the system and Mounting the partitions the right way.

Configuring mail delivery

If you want to automate the backups, i.e. invoke the backup commands from the Cron scheduler, or if you want to trigger the backup centrally from one point for the whole set of hosts (or potentially from more points for a few subsets of hosts) by using tar-lvm-all, you need to have your mail system configured properly on all hosts running the backup scripts. Cron must be able to deliver potential errors that cannot be written to the logs (e.g. if the backup storage isn't mounted yet) or do not belong there. And the same applies for the tar-lvm-one scripts invoked on each host by the tar-lvm-all script running centrally - some errors can be delivered just by email.

The tar-lvm-all script can be configured to instruct the backup scripts on each host not to use local MTA, but directly the SMTP protocol to connect to some mail server, but that's not the preferred solution. If the delivery fails, there's neither a way how to inform about the error nor any later repeated delivery attempt. The error output can get lost easily.

The simplest configuration of the whole mail system could be implemented as follows. Let's configure one host as the destination host for your email domain - either with separate mailboxes or with a catch-all mailbox depending on your needs. All other hosts could be configured to relay everything excluding mails to root to the destination server prepared earlier. This can be achieved relatively easily by using Postfix, for instance. If it is in default configuration that delivers emails to local mailboxes, it should be sufficient to add or update just a few configuration directives in the /etc/postfix/main.cf file.

A very simple example of Postfix directives to update on the mail destination host follows:

  • myhostname = smtp.domain.org
  • myorigin = domain.org
  • mydestination = domain.org smtp.domain.org localhost
  • mynetworks =
  • relayhost =

And now a very simple example for the host to backup:

  • myhostname = hostN.domain.org
  • myorigin = domain.org
  • mydestination =
  • mynetworks = 127.0.0.1/32
  • relayhost = [smtp.domain.org]

If you always want to keep mails to the localhost and hostN.domain.org domains locally, modify mydestination as follows:

  • mydestination = hostN.domain.og localhost

All emails to the root user can be then kept on the host by creating the file /etc/postfix/virtual with the following content:

  • root root@localhost

This change can be activated by invoking:

  • postmap /etc/postfix/virtual

And by adding virtual alias map directive to the main.cf configuration file.

  • virtual_alias_maps = hash:/etc/postfix/virtual

The servers shouldn't be opened for relaying using the described configuation, but it's wise to double-check and test your configuration to avoid this security issue.

Setting up SSH authentication

If the Tar-LVM scripts need to communicate with other hosts, they use the SSH protocol and they authenticate with SSH public key authentication. Such remote access is currently used in three different ways.

  • If the host being backed up stores the archives and logs to remote SSH storage, it uses the SSHFS protocol based on SFTP.
  • If the backup is triggered centrally for the whole set of hosts (or a few subsets) using tar-lvm-all, the backups are triggered and controlled via SSH on each host.
  • If the tar-lvm-one script for backing up one host uses shared configuration, this configuration is obtained from the shared location via SSH.

Authentication and authorization is always based on SSH public key authentication as mentioned earlier. This kind of authentication must be allowed on the server side by using the PubkeyAuthentication directive in the configuration file of the sshd daemon (e.g. in /etc/ssh/sshd_config).

  • PubkeyAuthenticaton yes

It can be also left out, because that's usually the default setting.

Configuration on the client side doesn't have to allow this kind of authentication - see the PubkeyAuthentication directive again in the configuration file of the ssh client (e.g. /etc/ssh/ssh_config). If it is set to no, it can be changed in the user's configuration file located in his home directory or directly on the command-line.

  • ssh -o PubkeyAuthentication=yes user@server

If public key authentication is allowed on both client and server side, a public/private key pair must be generated on the client side for correct user by using the ssh-keygen command. Ensure that you are logged in as the connecting user and don't protect the private key by any passphrase, because it is going to be used non-interactively.

  • ssh-keygen

After that place the generated public key (i.e. the contents of the file ~/.ssh/id_rsa.pub on the client side) as a new line to the file ~/.ssh/authorized_keys on the server side. Check again that you are working under the correct destination user account on the server side.

Your SSH access shouldn't require any password now, even if it is very well protected. If it still asks for one, check the permissions on the ~/.ssh directory that should be set to 700 or eventually on the file ~/.ssh/authorized_keys where 644 should be sufficient.

 

 
tar-lvm/install/etc/0000755002342000234200000000000013754303011013610 5ustar baxicbaxictar-lvm/install/etc/ssd-backup.conf0000644002342000234200000000255313754303011016520 0ustar baxicbaxic ### disable sysv (System V) or systemd (Systemd) init scripts support, ### just comment for the default behaviour, i.e. sysv/systemd support # format: nosysv ("true"|"false") # format: nosystemd ("true"|"false") #nosysv "true" #nosystemd "true" ### sysv or systemd services to stop or start depending on the mode # format: stopstart sysv|systemd [[pidfile=...][,][psname=...]] stopstart systemd sssd.service stopstart systemd cron.service stopstart systemd postfix.service stopstart systemd slapd.service stopstart systemd denyhosts.service stopstart sysv rsyslog stopstart systemd dbus.service ### extended regular expressions specifying the names of processes ### to kill in the stop mode (usually not services) # format: kill #kill "^console-kit-dae" ### real user names whose processes shouldn't be killed (should survive) ### when the -u option is used, i.e. when all non-root user processes ### should be killed (root is always included and doesn't have ### to be listed) # format: survruser survruser message+ survruser ntp ### commands to run at the end of the stop mode # format: stopcomm ... #stopcomm echo *** lsof /var #stopcomm lsof /var #stopcomm echo "SSD stopped..." ### commands to run at the beginning of the start mode # format: startcomm ... #startcomm echo "SSD starting..." tar-lvm/install/etc/tar-lvm/0000755002342000234200000000000013754042526015205 5ustar baxicbaxictar-lvm/install/etc/tar-lvm/tar-lvm-one.shared.conf0000644002342000234200000000246713754042526021473 0ustar baxicbaxic ### disable sshfs backup filesystem support, just comment for the default ### behaviour, i.e. both block device and sshfs support # format: nosshfs ("true"|"false") #nosshfs "true" ### backup device, either local device or remote sshfs filesystem, the local ### device is specified by name (not by whole path) or by UUID and the device ### must be located in the /dev directory, the remote sshfs filesystem is ### specified by the host and optional user and path # format: dev (|UUID=|[@]:[]) #dev "UUID=f3d286d1-aa4a-6f32-a367-ab93e72cbfa8" dev "root@baxic-nas.domain.org:/backup-data" ### device mapper name used by cryptsetup if the backup device is local and ### if it is encrypted by LUKS, comment the dm line if the backup device isn't ### encrypted # format: dm dm "backup" ### backup filesystem mount point # format: mntdir mntdir "/mnt/backup" ### directory on the backup filesystem containing the backup directory tree # format: rootdir rootdir "tar-lvm" ### defer ssd-backup start after the whole backup is complete or comment ### for the default behaviour, i.e. ssd-backup start after tar-lvm pre ### (ssd-backup stop, tar-lvm pre, ssd-backup start, tar-lvm run, tar-lvm post) # format: deferssdstart ("true"|"false") #deferssdstart "true" tar-lvm/install/etc/tar-lvm/tar-lvm.conf0000644002342000234200000000235613754042526017444 0ustar baxicbaxic ### suffix of the LVM snapshot names appended to the name of the origin # format: lvsnapsuffix lvsnapsuffix ".tar-lvm" ### disables ACLs support if set (necessary for older GNU/tar versions), ### just comment for the default behaviour, i.e. ACLs support # format: noacls ("true"|"false") #noacls "true" ### filesystems not on LVM # format: fs (|UUID=) [ ...] fs "boot" "UUID=621393c4-1827-4b6a-b053-1f249a844626" ### filesystems on top of LVM # format: lv % [ ...] lv "rootfs" "mg-baxic-prod" "20%" lv "usr" "mg-baxic-prod" "20%" lv "var" "mg-baxic-prod" "80%" lv "srv" "mg-baxic-prod" "80%" # don't backup tmp because it's only temporary location and its contents is # often deleted on system startup on some systems #lv "tmp" "mg-baxic-prod" "80%" lv "vartmp" "mg-baxic-prod" "80%" lv "varmail" "mg-baxic-prod" "80%" # don't backup varlock because it's only temporary location and its contents # is often deleted on system startup on some systems, moreover, varlock cannot # be remounted read-only because LVM creates file locks in it during snapshot # creation #lv "varlock" "mg-baxic-prod" "80%" lv "home" "mg-baxic-prod" "20%" "./baxic/data" tar-lvm/install/etc/tar-lvm/tar-lvm-all.conf0000644002342000234200000000517413754042526020213 0ustar baxicbaxic ### backup device on the physical machine if at least one backup to local ### device should be performed (not needed for SSHFS), the device is ### specified by name (not by whole path) or by UUID and it must be ### be located in the /dev directory, it can be either whole disk, ### partition or volume etc. # format: pmdev (|UUID=) pmdev "UUID=0da345bc" ### backup device to create on the virtual machines if at least one backup ### to local device should be performed (not needed for SSHFS), the device ### is specified by name (not by whole path) ### format: vmdev vmdev "vdb" ### set to "true" to enable password prompt, the prompt is needed if the backup ### device is encrypted and a password should be therefore passed to tar-lvm-one ### (for each virtual machine), just comment if not needed # format: passprompt ("true"|"false") passprompt "true" ### set to "true" if you want to backup the physical machine as well, otherwise ### comment or set to "false", the machine must be accessible as localhost by ssh ### public key authentication, i.e. without using a password # format: pmbackup ("true"|"false") pmbackup "true" ### names of the virtual machines to backup, the names are hostnames as well, ### but you can specify different hostname appended to the machine name ### behind a colon, e.g. as "machine:host.domain.org", the machines must ### be accessible by ssh public key authentication, i.e. without using ### a password # format: vm [:] vm "baxic-prod" vm "baxic-prod-old" ### hosts to backup using sshfs, the names are hostnames as well, but ### you can specify different hostname appended to the host name ### behind a colon, e.g. as "host:host.domain.org", hosts must be accessible ### by ssh public key authentication, i.e. without using a password # format: host [:] host "baxic-test-1" host "baxic-test-2" host "baxic-test-3" ### number of hosts to backup in parallel, remaining hosts must wait until ### the preceding backups finish, this setting doesn't apply to vm's (i.e. ### virtual machines) that are always backed up one by one # format: parhostnum parhostnum 2 ### notification email addresses ### format: mailto mailto "backup@domain.org" ### SMTP server if neither mail nor mailx (i.e. local MTA) should be used ### and SMTP should be used directly, simply comment if local MTA should be ### used instead # format: smtp[-tls|s]://[USER:PASS@]SERVER[:PORT]] #smtpserver "smtp-tls://user@gmail.com:secret@smtp.googlemail.com" ### notification email sender if smtpserver is specified # format: mailfrom mailfrom "user@gmail.com" tar-lvm/install/etc/tar-lvm/tar-lvm-one.local.conf0000644002342000234200000000067013754042526021311 0ustar baxicbaxic ### hostname of the machine that contains the shared configuration file ### (and usually runs the tar-lvm-all script if used - hence the name), ### the machine must be accessible by ssh public key authentication, i.e. ### without using a password, comment the allhost line if no shared ### configuration file should be copied to localhost and used # format: allhost [:] allhost "baxic-pm:baxic-kvm-1.domain.org" tar-lvm/install/sbin/0000755002342000234200000000000013754042526014003 5ustar baxicbaxictar-lvm/install/sbin/bgrun0000755002342000234200000000515414041736603015047 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: bgrun # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### PROGNAME='bgrun' PNSPACES=' ' # set versions SCRIPT_VERSION='00.20' SUITE_VERSION='00.80' # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN are available on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 1 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 1 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - run process on background and send its output by mail" echo echo " ($PROGNAME version: $SCRIPT_VERSION, tar-lvm suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h" echo " $PROGNAME [ [[FROM-ADDR] smtp[-tls|s]://[USER:PASS@]SERVER[:PORT]] \\" echo " $PNSPACES TO-ADDR-1 ... ] -- COMMAND [ARGUMENTS]" echo exit "$ec" } ###### MAIN ################################################################### ### check if all the external tools are available on the system check_tools cmdsend # parse and check arguments [[ $# -lt 1 ]] && usage 1 [[ $# -eq 1 && "$1" = '-h' ]] && usage # run cmdsend on background, cmdsend runs the specified command, # redirects its output to a file and mails it at the end to the specified # email addresses cmdsend "$@" & # wait so that cmdsend can report errors about its incorrect usage # and all ssh commands are finished (e.g. by tar-lvm-one when mounting # the sshfs filesystems and ssh-agent is in use) sleep 10 # always clean exit, we cannot know the exit status of the running command yet exit 0 tar-lvm/install/sbin/ssd-backup0000755002342000234200000007673114041736641016001 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: ssd-backup # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### # set program name PROGNAME='ssd-backup' PNSPACES=' ' # set versions SCRIPT_VERSION='00.22' SUITE_VERSION='00.80' # set configuration directory and default configuration file WORKDIR="/var/local/$PROGNAME" CONFFILE_DEFAULT="/usr/local/etc/$PROGNAME.conf" # set default values that might be redefined in the configuration file unset NOSYSV unset NOSYSTEMD unset SSTYPE unset SSINIT unset SSOPTS unset KILLPS SURVRUSERS=root STOPCOMMS_N=0 STARTCOMMS_N=0 # exit commands array and clean exit running flag unset EXIT_CMDS CE_RUNNING=1 # script result as a string, i.e. its exit status printed at the end RESULT='FAILURE' # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN are available on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 2 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 2 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - stop/start daemons before/after backup" echo echo " ($PROGNAME version: $SCRIPT_VERSION, tar-lvm suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h|help" echo " $PROGNAME [-u] [-v] [-f] [-c cfgfile] (stop|start)" echo " $PROGNAME [-v] [-c cfgfile] clean" echo echo ' -h|help ... print help and exit' echo ' stop ... stop the sysv/systemd services and kill the processes' echo ' specified in the config file' echo ' start|clean ... start the sysv/systemd services specified' echo ' in the config file' echo ' -u ... kill also all user processes (stop mode only) and' echo ' restrict/permit user logins by creating/removing' echo " the file '/var/run/nologin'" echo ' -f ... force start/stop action for all configured services' echo ' irrespective of their initial status' echo ' -v ... verbosely print what is done' echo ' -c cfgfile ... path to the non-default configuration file' echo " (default: '$CONFFILE_DEFAULT')" echo exit "$ec" } # parse_args() arg1 ... argN # parse the command-line arguments and set the global variables ARG_MODE, # ARG_U, ARG_V, ARG_F, ARG_C and ARG_C_VAL # return: exit - wrong arguments are given # 0 - ok parse_args() { local arg ARGNUM=0 ARG_MODE='' ARG_U='n' ARG_V='n' ARG_F='n' ARG_C='n' unset ARG_C_VAL unset argvalfor for arg in "$@"; do if [[ -z "$argvalfor" ]]; then case "$arg" in '-v') [[ "$ARG_V" = 'y' ]] && usage 1 ARG_V='y' ;; '-u') [[ "$ARG_U" = 'y' ]] && usage 1 ARG_U='y' ;; '-f') [[ "$ARG_F" = 'y' ]] && usage 1 ARG_F='y' ;; '-c') [[ "$ARG_C" = 'y' ]] && usage 1 ARG_C='y' argvalfor='-c' ;; *) if [[ "${arg:0:1}" = '-' \ && ( "$arg" != '-h' || "$ARGNUM" -gt 0 ) ]] then usage 1 else ARGNUM="$((ARGNUM+1))" case "$ARGNUM" in 1) case "$arg" in '-h' | 'help') ARG_MODE='help' ;; 'stop' | 'start' | 'clean') ARG_MODE="$arg" ;; *) usage 1 ;; esac ;; *) usage 1 ;; esac fi ;; esac else case "$argvalfor" in '-c') ARG_C_VAL="$arg"; ;; *) usage 1 ;; esac unset argvalfor fi done [[ -n "$argvalfor" ]] && usage 1 if [[ -z "$ARG_MODE" ]]; then usage 1 elif [[ "$ARG_MODE" = 'help' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_V" = 'y' || "$ARG_U" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'stop' || "$ARG_MODE" = 'start' ]]; then [[ "$ARGNUM" -ne 1 ]] && usage 1 elif [[ "$ARG_MODE" = 'clean' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_U" = 'y' || "$ARG_F" = 'y' ]] && usage 1 fi if [[ "$ARG_MODE" = 'clean' ]]; then ARG_U='y' fi return 0 } # add_exit_cmd exit_cmd # add exit command to be invoked when clean_exit is invoked, the command # is just one argument to be invoked via eval # return: 0 - ok add_exit_cmd() { local cmd="$1" local i i="${#EXIT_CMDS[@]}" EXIT_CMDS[i]="$cmd" return 0; } # del_exit_cmd [exit_cmd_number] # remove exit command to be invoked when clean_exit is invoked, the command # number can be specified, if it is not, the last command is removed if any # return: 0 - ok del_exit_cmd() { local i="$1" local j if [[ "${#EXIT_CMDS[@]}" -gt 0 ]]; then if [[ -z "$i" ]]; then j="$(( ${#EXIT_CMDS[@]} - 1 ))" unset EXIT_CMDS[j] else EXIT_CMDS[i]='' fi fi return 0; } # run_exit_cmds # invoke all registered exit commands in the opposite order in which they # were registered, but do not actually exit, then remove all exit commands # return: 0 - ok # 1 - some exit command failed run_exit_cmds() { local i n local cmd local eval_ec ec=0 n="${#EXIT_CMDS[@]}" for ((i=n-1; i>=0; i--)); do cmd="${EXIT_CMDS[i]}" if [[ -n "$cmd" ]]; then eval "$cmd" eval_ec="$?" fi if [[ "$eval_ec" -ne 0 ]]; then ec=1 fi done unset EXIT_CMDS return "$ec" } # clean_exit [ec] # exit cleanly, i.e. invoke all registered exit commands before exiting # in the opposite order in which they were registered, the exit commands # can be (de)registered by using add_exit_cmd/del_exit_cmd, finally, # exit with the given exit code or with 1 if no exit code is given # and some command fails # return: exit clean_exit() { local ec_orig="${1:-0}" local i n local cmd local ec=0 CE_RUNNING=0 run_exit_cmds [[ "$?" -ne 0 ]] && ec=1 [[ "$ec_orig" = 0 && "$ec" != 0 ]] && ec_orig="$ec" exit "$ec_orig" } # print_result() # print the final status of the script, i.e. its result # return: 0 - ok print_result() { echo echo "$PROGNAME: result => $RESULT (not checked by tar-lvm suite)" echo } # check_root_id() # check that the command is called by root # return: exit - the command is not called by root # 0 - ok check_root_id() { if [[ $(id -u) != 0 ]]; then echo "$PROGNAME: you must be root in order to use this script" >&2 clean_exit 2 fi return 0 } # check_work_dir # check that the working directory and has proper access permissions # return: exit - the working directory isn't all right # 0 - ok check_work_dir() { local ok=0 if [[ ! -d "$WORKDIR" || ! -r "$WORKDIR" || ! -w "$WORKDIR" ]]; then echo "$PROGNAME: the directory '$WORKDIR' doesn't exist or" >&2 echo "$PNSPACES isn't readable and writable" >&2 ok=1 fi [[ "$ok" != 0 ]] && clean_exit 2 return 0 } # get_command() outarr ('startcomm'|'stopcomm') command arg1 ... argN # get command with optional arguments and store it as the last entry # of the array variable with name outarr # return: 0 - ok get_command() { local outarr="$1" shift 2 eval $outarr='("$@")' return 0 } # read_config_file cfg_file # read the configuration file cfg_file, each line of the configuration file # which is not a comment consists of whitespace separated words, the words # can be optionally enclosed in double quotes and can therefore also contain # whitespace characters - space, tab, double quote and backslash may be part # of a word too if they're escaped by a backslash, lines starting with # # (optional whitespaces may precede #) are comments, currently only # the nosysv, nosystemd, survruser, stopstart, kill, startcomm and stopcomm # lines are supported, they are used to define the NOSYSV, NOSYSTEMD, # SURVRUSERS, SSINIT, SSTYPE, SSOPTS, KILLPS, STOPCOMMS_* and STARTCOMMS_* # global variables # return: exit - the config file isn't readable or doesn't have correct format # 0 - ok read_config_file() { local cfg_file="$1" cfg_cont local linenum line str fch wrd local ifs local n i local err opts local isnosysv=1 isnosystemd=1 # check that the config file exists and is readable if [[ -f "$cfg_file" && -r "$cfg_file" ]]; then cfg_cont="$(cat "$cfg_file")" else echo "$PROGNAME: cannot read the configuration file '$cfg_file'" >&2 clean_exit 2 fi # remove space, tab and newline from IFS so that the whole line # including the leading whitespace is read by the read built-in ifs="$IFS" IFS='' # read and process all lines from the file cfg_file linenum=0 while read -r line; do linenum="$((linenum+1))" unset args local -a args # remove comments, i.e. lines beginning with #, optional whitespace # may precede the # character str="$(echo "$line" | sed -r 's/^([[:blank:]]*)#.*$/\1/')" # read words from the line read and store them into the args array while true; do # remove leading whitespace str="$(echo "$str" | sed -nr 's#^[[:blank:]]*(.*)?$#\1#p')" # if there is no first character, i.e. no word, then break fch="${str:0:1}" if [[ -z "$fch" ]]; then break # if the first character isn't ", the word is delimited # by whitespace that is not escaped by \ elif [[ "$fch" != '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\4#p') # if the first character is ", the word is delimited by # double quotes that are not escaped by \ and the quoted words # by whitespace elif [[ "$fch" = '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\4#p') fi # replace all characters escaped by \ in the word read wrd="$(echo "$wrd" | sed -r 's#[\\]([[:blank:]"\\])#\1#g')" # store the resulting word into the args array args[${#args[*]}]="$wrd" done # if the line has at least one word, check if it is correct and # assign values to appropriate variables if [[ ${#args[*]} -gt 0 ]]; then err=1 case "${args[0]}" in 'nosysv') if [[ "$isnosysv" = 0 ]]; then echo "$PROGNAME: nosysv defined multiple times in the config file" >&2 clean_exit 2 fi isnosysv=0 if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi NOSYSV="${args[1]}" ;; 'nosystemd') if [[ "$isnosystemd" = 0 ]]; then echo "$PROGNAME: nosystemd defined multiple times in the config file" >&2 clean_exit 2 fi isnosystemd=0 if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi NOSYSTEMD="${args[1]}" ;; 'stopstart') [[ ${#args[*]} -lt 3 || ${#args[*]} -gt 4 || -z "${args[1]}" || ( "${args[1]}" != 'systemd' && "${args[1]}" != 'sysv' ) || -z "${args[2]}" ]] && err=0 opts="$(echo "${args[3]}" | tr ',' '\n')" [[ "$(echo "$opts" | grep -Ev '^pidfile=|psname=' | wc -l)" -gt 2 ]] && err=0 [[ "$(echo "$opts" | grep -Ev '^pidfile=' | wc -l)" -gt 1 ]] && err=0 [[ "$(echo "$opts" | grep -Ev '^psname=' | wc -l)" -gt 1 ]] && err=0 if [[ "$err" = 0 ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#SSINIT[*]}" SSTYPE[n]="${args[1]}" SSINIT[n]="${args[2]}" SSOPTS[n]="${args[3]}" ;; 'kill') if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#KILLPS[*]}" KILLPS[n]="${args[1]}" ;; 'survruser') if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi if [[ -z "$SURVRUSERS" ]]; then SURVRUSERS="${args[1]}" else SURVRUSERS="$SURVRUSERS|${args[1]}" fi ;; 'stopcomm') if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi get_command STOPCOMMS_$((STOPCOMMS_N++)) "${args[@]}" ;; 'startcomm') if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi get_command STARTCOMMS_$((STARTCOMMS_N++)) "${args[@]}" ;; *) echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 ;; esac fi done <&2 echo "$PNSPACES at least one stopstart directive of type sysv exists" >&2 clean_exit 2 fi if [[ "$nosystemd_err" = 0 ]]; then echo "$PROGNAME: nosystemd directive specified in the config file, but" >&2 echo "$PNSPACES at least one stopstart directive of type systemd exists" >&2 clean_exit 2 fi return 0 } # check_addon_tools() # check if add-on tools that are necessary for proper operation are present # in the system, the specific tools depend on given script arguments # or configuration # return: exit - some of the tools are missing # 0 - ok check_addon_tools() { [[ "$CE_RUNNING" = 0 ]] && return 0 local ec=0 # check that service binary exists if no sysv directive is specified if [[ -z "$NOSYSV" ]]; then if ! which service >/dev/null; then echo "$PROGNAME: 'service' binary not found and sysv not disabled" >&2 echo "$PNSPACES in the config file" >&2 ec=2 fi fi # check that systemctl binary exists if no systemd directive is specified if [[ -z "$NOSYSTEMD" ]]; then if ! which systemctl >/dev/null; then echo "$PROGNAME: 'systemctl' binary not found and systemd not disabled" >&2 echo "$PNSPACES in the config file" >&2 ec=2 fi fi # check that the pkill binary is present if some processed should be killed if [[ "${#KILLPS[@]}" -gt 0 || "$ARG_U" = 'y' ]]; then if ! which pkill >/dev/null; then echo "$PROGNAME: 'pkill' binary not found and some processes" >&2 echo "$PNSPACES should be killed as specified in the config file" >&2 echo "$PNSPACES or by the -u argument" >&2 ec=2 fi fi # check that the pgrep binary is present if some processed should be killed if [[ "${#KILLPS[@]}" -gt 0 ]]; then if ! which pgrep >/dev/null; then echo "$PROGNAME: 'pgrep' binary not found and some processes" >&2 echo "$PNSPACES should be killed as specified in the config file" >&2 ec=2 fi fi [[ "$ec" -ne 0 ]] && clean_exit "$ec" return 0 } # get_timestamp # print timestamp in the RFC-3339 format # return: # 0 - ok get_timestamp() { if [[ "$ARG_V" = 'y' ]]; then echo -n "$PROGNAME: " date --rfc-3339=seconds fi return 0 } # check_status_file() # check that the ssd-backup status file is available in the workdir # return: exit - the file isn't available, readable or writable # 0 - ok check_status_file() { if [[ "$ARG_MODE" = 'stop' ]]; then if [[ -f "$WORKDIR/status" ]]; then echo "$PROGNAME: the file '$WORKDIR/status' already exists which" >&2 echo "$PNSPACES indicates that this command in the stop mode has already" >&2 echo "$PNSPACES been invoked, use this command in the start mode to revert" >&2 echo "$PNSPACES to the original state and rather check if all the services" >&2 echo "$PNSPACES are running (this might not be the case if you ran this command" >&2 echo "$PNSPACES in the stop mode some time ago)" >&2 clean_exit 2 fi elif [[ "$ARG_MODE" = 'start' ]]; then if [[ ! -f "$WORKDIR/status" || ! -r "$WORKDIR/status" ]]; then echo "$PROGNAME: the file '$WORKDIR/status' isn't available or readable," >&2 echo "$PNSPACES this indicates that this command in the stop mode hasn't finished" >&2 echo "$PNSPACES successfully prior to the start mode and you therefore cannot" >&2 echo "$PNSPACES use the start mode now, you must check and start the services" >&2 echo "$PNSPACES manually by using the ps, service and systemctl commands" >&2 echo "$PNSPACES if you want to do so or use this command with the -f switch" >&2 clean_exit 2 fi fi return 0 } # stop() # stop the sysv/systemd services and kill the processes specified # in the config file, also restrict user logins and kill all user processes # if the -u option was given # return: 0 - ok (the return value of the sysv scripts doesn't always indicate # the success or failure) # 1 - cannot restrict user logins # 2 - cannot find out the status of sysv services # 3 - error when stopping some service(s) or killing some process(es), # all stopping tasks are always run irrespective of potential # errors in some of them stop() { local i n local service signal local ec=0 statall_ec io_ec local stat active pidfile psname pf_pid pf_name # restrict user logins if -u was given rm -f "$WORKDIR/nologin" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete the file '$WORKDIR/nologin' and thus remember" >&2 echo "$PNSPACES user logins, delete it manually" >&2 return 1 fi if [[ -f '/var/run/nologin' ]]; then touch "$WORKDIR/nologin" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create the file '$WORKDIR/nologin' and thus remember" >&2 echo "$PNSPACES user logins status" >&2 fi fi if [[ "$ARG_U" = 'y' ]]; then if [[ ! -f '/var/run/nologin' ]]; then [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: restricting user logins by creating the file '/var/run/nologin'" touch '/var/run/nologin' if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create the file '/var/run/nologin' and thus restrict" >&2 echo "$PNSPACES user logins, aborting..." >&2 return 1 fi else if [[ "$ARG_V" = 'y' ]]; then echo "$PROGNAME: user logins already restricted, the file '/var/run/nologin'" echo "$PNSPACES exists" fi fi fi # create the status file for all available system sysv services rm -f "$WORKDIR/service.status-all" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete the old file '$WORKDIR/service.status-all'" >&2 echo "$PNSPACES and thus find out the status of sysv services, aborting..." >&2 return 2 fi service --status-all >"$WORKDIR/service.status-all" 2>&1 statall_ec="$?" grep -Ev '^[[:blank:]]*\[[[:blank:]]*[?+-][[:blank:]]*\][[:blank:]]+[^[:blank:]]' "$WORKDIR/service.status-all" >&2 if [[ "$statall_ec" -ne 0 ]]; then echo "$PROGNAME: cannot create the file '$WORKDIR/service.status-all'" >&2 echo "$PNSPACES and thus find out the status of sysv services, aborting..." >&2 return 2 fi # delete the status file if -f argument was given if [[ "$ARG_F" = 'y' ]]; then rm -f "$WORKDIR/status" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete the file '$WORKDIR/status' and thus purge" >&2 echo "$PNSPACES stale status of services, delete it manually" >&2 fi fi # stop the sysv/systemd services if they exist n="${#SSINIT[@]}" for ((i=0; i&2 ec=3 continue fi stat="$(sed -rn "s#^[[:blank:]]*\[[[:blank:]]*([?+-])[[:blank:]]*\][[:blank:]]+$service([[:blank:]]*)?\$#\1#p" "$WORKDIR/service.status-all")" if [[ "$stat" = '+' || "$stat" = '?' || "$ARG_F" = 'y' ]]; then if [[ "$stat" = '?' && -n "${SSOPTS[i]}" ]]; then pidfile="$(echo "${SSOPTS[i]}" | tr ',' '\n' | sed -rn 's#^pidfile=(.*)$#\1#p')" psname="$(echo "${SSOPTS[i]}" | tr ',' '\n' | sed -rn 's#^psname=(.*)$#\1#p')" if [[ -n "$pidfile" ]]; then if [[ -f "$pidfile" && -r "$pidfile" ]]; then pf_pid="$(cat "$pidfile")" if [[ -n "$pf_pid" ]]; then pf_name="$(ps -eo pid,comm= | sed -rn "s#^[[:blank:]]*$pf_pid[[:blank:]]+(.*)\$#\1#p")" if [[ -n "$psname" ]]; then if [[ "$pf_name" = "$psname" ]]; then stat='+' else stat='-' fi else if [[ -n "$pf_name" ]]; then stat='+' else stat='-' fi fi else stat='-' fi else stat='-' fi else if [[ -n "$psname" ]]; then ps_name="$(ps -eo comm= | sed -rn "s#^($psname)\$#\1#p")" if [[ -n "$ps_name" ]]; then stat='+' else stat='-' fi fi fi fi [[ "$stat" = '-' ]] && continue [[ "$ARG_V" = 'y' && "$stat" = '?' ]] \ && echo "$PROGNAME: cannot find out if sysv service '$service' is running" [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: stopping the '$service' sysv service" echo "$stat sysv $service" >>"$WORKDIR/status" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot store initial status of sysv service '$service'" >&2 echo "$PNSPACES into the file '$WORKDIR/status'" >&2 fi service "$service" stop >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: error during the sysv service '$service' shutdown" >&2 ec=3 continue fi fi ;; 'systemd') systemctl -t service list-units --all --no-legend | awk '{print($1);}' | grep -Eq "^$service\$" if [[ $? -ne 0 ]]; then echo "$PROGNAME: the systemd service '$service' doesn't exist" >&2 ec=3 continue fi systemctl is-active "$service" >/dev/null active=$? if [[ "$active" -eq 0 || "$ARG_F" = 'y' ]]; then [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: stopping the '$service' systemd service" if [[ "$active" -eq 0 ]]; then echo "+ systemd $service" >>"$WORKDIR/status" io_ec=$? else echo "- systemd $service" >>"$WORKDIR/status" io_ec=$? fi if [[ "$io_ec" -ne 0 ]]; then echo "$PROGNAME: cannot store initial status of systemd service '$service'" >&2 echo "$PNSPACES into the file '$WORKDIR/status'" >&2 fi systemctl stop "$service" >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: error during the systemd service '$service' shutdown" >&2 ec=3 continue fi fi ;; esac done # send the processes to kill the SIGTERM signal, wait 3 seconds # and send them the SIGKILL signal to ensure they get stopped for signal in SIGTERM SIGKILL; do n="${#KILLPS[@]}" for ((i=0; i/dev/null [[ $? -ne 0 ]] && continue pkill "-$signal" "${KILLPS[i]}" if [[ $? -ne 0 ]]; then echo "$PROGNAME: error when killing the '${KILLPS[i]}' processes" >&2 ec=3 continue fi done if [[ "$ARG_U" = 'y' ]]; then if [[ "$ARG_V" = 'y' ]]; then echo "$PROGNAME: sending all user processes the $signal signal," echo "$PNSPACES i.e. to all processes whose real username isn't one of:" echo "$PNSPACES ${SURVRUSERS//|/,}" fi KILLRUSERS="$(ps -eo ruser= | grep -Ev "^$SURVRUSERS\$" | sort | uniq | tr '\n' ,)" KILLRUSERS="${KILLRUSERS%,}" if [[ -n "$KILLRUSERS" ]]; then pkill -"$signal" -U "$KILLRUSERS" if [[ $? -ne 0 ]]; then echo "$PROGNAME: error when killing the processes not invoked by '$SURVRUSERS'" >&2 ec=3 fi fi fi if [[ "$signal" = "SIGTERM" && $n -ne 0 ]]; then [[ "$ARG_V" = 'y' ]] && echo "$PROGNAME: waiting 3 seconds" sleep 3 fi done # invoke all commands for the stop mode for ((i=0; i&2 fi return "$ec" } # start() # start the sysv/systemd scripts specified in the config file, also permit # user logins if the -u option was given # return: 0 - ok (the return value of the sysv scripts doesn't always indicate # the success or failure) # 1 - cannot permit user logins or forget user logins status # 2 - error when starting some service(s), all starting tasks are # always run irrespective of potential errors in some of them start() { local i local service local ec=0 local stat # invoke all commands for the start mode for ((i=0; i=0; i--)); do service="${SSINIT[i]}" type="${SSTYPE[i]}" if [[ "$ARG_F" = 'n' ]]; then stat="$(sed -rn "s#^(.)[[:blank:]]+[^[:blank:]]+[[:blank:]]+$service\$#\1#p" "$WORKDIR/status")" else stat='+' fi case "${SSTYPE[i]}" in 'sysv') if [[ ! -x "/etc/init.d/$service" ]]; then echo "$PROGNAME: the sysv service '$service' doesn't exist" >&2 ec=2 continue fi if [[ "$stat" = '+' || "$stat" = '?' ]]; then if [[ "$ARG_V" = 'y' && "$stat" = '?' ]]; then echo "$PROGNAME: cannot find out if sysv service '$service'" echo "$PNSPACES was running during the stop phase" fi [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: starting the '$service' sysv service" service "$service" start >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: error during the sysv service '$service' startup" >&2 ec=2 continue fi fi ;; 'systemd') systemctl -t service list-units --all --no-legend | awk '{print($1);}' | grep -Eq "^$service\$" if [[ $? -ne 0 ]]; then echo "$PROGNAME: the systemd service '$service' doesn't exist" >&2 ec=2 continue fi if [[ "$stat" = '+' || "$stat" = '?' ]]; then if [[ "$ARG_V" = 'y' && "$stat" = '?' ]]; then echo "$PROGNAME: cannot find out if systemd service '$service'" echo "$PNSPACES was running during the stop phase" fi [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: starting the '$service' systemd service" systemctl start "$service" >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: error during the systemd service '$service' startup" >&2 ec=2 continue fi fi ;; esac done else echo "$PROGNAME: the file '$WORKDIR/status' doesn't exist which" >&2 echo "$PNSPACES indicates that this command in the stop mode hasn't been invoked" >&2 echo "$PNSPACES prior to the start or clean mode, no services are going to be" >&2 echo "$PNSPACES started" >&2 fi # delete the status file rm -f "$WORKDIR/status" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot remove the file '$WORKDIR/status'," >&2 echo "$PNSPACES check and remove it manually!!! this is important" >&2 echo "$PNSPACES because the existence of this file indicates the state" >&2 echo "$PNSPACES of the $PROGNAME cycle, i.e. whether the stop" >&2 echo "$PNSPACES $PROGNAME action has been executed" >&2 return 1 fi # permit user logins if -u was given if [[ "$ARG_U" = 'y' ]]; then if [[ ! -f "$WORKDIR/nologin" ]]; then if [[ -f '/var/run/nologin' ]]; then [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: permitting user logins by deleting the file '/var/run/nologin'" rm -f '/var/run/nologin' if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete the file '/var/run/nologin' and thus permit" >&2 echo "$PNSPACES user logins, delete it manually" >&2 ec=1 fi else if [[ "$ARG_V" = 'y' ]]; then echo "$PROGNAME: user logins already permitted, the file '/var/run/nologin'" echo "$PNSPACES doesn't exist" fi fi fi fi rm -f "$WORKDIR/nologin" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete the file '$WORKDIR/nologin' and thus" >&2 echo "$PNSPACES forget user logins status, delete it manually" >&2 ec=1 fi return "$ec" } ###### MAIN ################################################################### ### check if all the external tools are available on the system check_tools id true cat sed grep date rm touch ### parse and check arguments, run initialization parse_args "$@" [[ "$ARG_MODE" = 'help' ]] && usage # check if the script is called by root check_root_id # check that the working directory is all right check_work_dir # read and check the configuration file CONFFILE="$CONFFILE_DEFAULT" [[ "$ARG_C" = 'y' ]] && CONFFILE="$ARG_C_VAL" read_config_file "$CONFFILE" check_config # check add-on tools presence check_addon_tools ### run the stop or start mode EC=0 get_timestamp add_exit_cmd 'print_result' add_exit_cmd 'get_timestamp' [[ "$ARG_F" = 'n' ]] && check_status_file if [[ "$ARG_MODE" = 'stop' ]]; then stop EC="$?" elif [[ "$ARG_MODE" = 'start' || "$ARG_MODE" = 'clean' ]]; then start EC="$?" fi ### exit if [[ "$EC" -eq 0 ]]; then RESULT='OK' else EC=0 fi clean_exit "$EC" tar-lvm/install/sbin/tar-lvm-one0000755002342000234200000014114614041736722016077 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: tar-lvm-one # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### # set program name PROGNAME='tar-lvm-one' PNSPACES=' ' # set versions SCRIPT_VERSION='00.22' SUITE_VERSION='00.80' # set configuration directory and files CONFDIR='/usr/local/etc/tar-lvm' WORKDIR='/var/local/tar-lvm' CONFFILE_LOCAL='tar-lvm-one.local.conf' CONFFILE_SHARED='tar-lvm-one.shared.conf' # set uuid filename UUID_FILE="uuid.list.$PROGNAME" # set tmpfs size in bytes TMPFS_SIZE="$((20*1024*1024))" # exit commands array and clean exit running flag unset EXIT_CMDS CE_RUNNING=1 # script result as a string, i.e. its exit status printed at the end RESULT='FAILURE' # configuration variables, i.e. physical machine/host, no sshfs flag, backup # device, device mapper name if backup device is encrypted, mountpoint # and backup root directory on the backup filesystem unset ALLHOST unset NOSSHFS unset DEV unset DM unset MNTDIR unset ROOTDIR unset DEFERSSDSTART # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN and which are available # on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 1 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 1 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - tar-lvm and ssd-backup wrapper for one machine" echo echo " ($PROGNAME version: $SCRIPT_VERSION, tar-lvm suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h | help" echo " $PROGNAME pre level" echo " $PROGNAME [-f] run level" echo " $PROGNAME post level" echo " $PROGNAME [-f] all level" echo " $PROGNAME check level" echo " $PROGNAME clean" echo echo ' -h | help ... print help and exit' echo ' pre ... prepare the system for the backup, i.e. mount backup disk,' echo " run 'ssd-backup -u stop', 'tar-lvm pre' and 'ssd-backup -u start'" echo ' and umount backup disk' echo " run ... mount backup disk, run 'tar-lvm run level outdir' and" echo ' unmount backup disk' echo " post ... finalize the backup, i.e. mount backup disk, run 'tar-lvm post'" echo ' unmount backup disk' echo ' all ... all phases, i.e. pre, run and post in this order, backup disk' echo ' is mounted and unmounted just once' echo ' check ... check the result of the backup of the specified level' echo ' and of all preceding levels' echo ' clean ... perform cleanup, this may be needed if the script terminates' echo ' unexpectedly, e.g. it it is killed by a signal, if the system' echo ' is restarted or halted etc.' echo ' level ... level of the incremental backup, i.e. non-negative integer' echo " -f ... force the backup, pass this argument to 'tar-lvm'" echo exit "$ec" } # parse_args() arg1 ... argN # parse the command-line arguments and set the global variables ARG_MODE, # ARG_LEVEL and ARG_F # return: exit - wrong arguments are given # 0 - ok parse_args() { local arg ARGNUM=0 ARG_MODE='' ARG_LEVEL='' ARG_F='n' for arg in "$@"; do case "$arg" in '-f') [[ "$ARG_F" = 'y' ]] && usage 1 ARG_F='y' ;; *) if [[ "${arg:0:1}" = '-' \ && ( "$arg" != '-h' || "$ARGNUM" -gt 0 ) ]]; then usage 1 else ARGNUM="$((ARGNUM+1))" case "$ARGNUM" in 1) case "$arg" in '-h' | 'help') ARG_MODE='help' ;; 'pre' | 'run' | 'post' | 'all' | 'clean' | 'check') ARG_MODE="$arg" ;; *) usage 1 ;; esac ;; 2) ARG_LEVEL="$arg" ;; esac fi ;; esac done if [[ -z "$ARG_MODE" ]]; then usage 1 elif [[ "$ARG_MODE" = 'help' || "$ARG_MODE" = 'clean' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_F" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'pre' || "$ARG_MODE" = 'post' || "$ARG_MODE" = 'check' ]]; then [[ "$ARGNUM" -ne 2 || "$ARG_F" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'run' || "$ARG_MODE" = 'all' ]]; then [[ "$ARGNUM" -ne 2 ]] && usage 1 fi return 0 } # check_args() # check that the command-line arguments are correct, must be called after # parse_args # return: exit - the arguments are not correct # 0 - ok check_args() { if [[ "$ARG_MODE" = 'pre' || "$ARG_MODE" = 'run' || "$ARG_MODE" = 'post' || "$ARG_MODE" = 'all' || "$ARG_MODE" = 'check' ]]; then if ! echo "$ARG_LEVEL" | grep -Eq '^[0-9]+$'; then echo "$PROGNAME: level must be a non-negative integer" >&2 exit 1 fi fi return 0 } # add_exit_cmd exit_cmd # add exit command to be invoked when clean_exit is invoked, the command # is just one argument to be invoked via eval # return: 0 - ok add_exit_cmd() { local cmd="$1" local i i="${#EXIT_CMDS[@]}" EXIT_CMDS[i]="$cmd" return 0; } # del_exit_cmd [exit_cmd_number] # remove exit command to be invoked when clean_exit is invoked, the command # number can be specified, if it is not, the last command is removed if any # return: 0 - ok del_exit_cmd() { local i="$1" local j if [[ "${#EXIT_CMDS[@]}" -gt 0 ]]; then if [[ -z "$i" ]]; then j="$(( ${#EXIT_CMDS[@]} - 1 ))" unset EXIT_CMDS[j] else EXIT_CMDS[i]='' fi fi return 0; } # run_exit_cmds # invoke all registered exit commands in the opposite order in which they # were registered, but do not actually exit, then remove all exit commands # return: 0 - ok # 1 - some exit command failed run_exit_cmds() { local i n local cmd local eval_ec ec=0 n="${#EXIT_CMDS[@]}" for ((i=n-1; i>=0; i--)); do cmd="${EXIT_CMDS[i]}" if [[ -n "$cmd" ]]; then eval "$cmd" eval_ec="$?" fi if [[ "$eval_ec" -ne 0 ]]; then ec=1 fi done unset EXIT_CMDS return "$ec" } # clean_exit [ec] # exit cleanly, i.e. invoke all registered exit commands before exiting # in the opposite order in which they were registered, the exit commands # can be (de)registered by using add_exit_cmd/del_exit_cmd, finally, # exit with the given exit code or with 1 if no exit code is given # and some command fails # return: exit clean_exit() { local ec_orig="${1:-0}" local i n local cmd local ec=0 CE_RUNNING=0 run_exit_cmds [[ "$?" -ne 0 ]] && ec=1 [[ "$ec_orig" = 0 && "$ec" != 0 ]] && ec_orig="$ec" exit "$ec_orig" } # print_result() # print the final status of the script, i.e. its result # return: 0 - ok print_result() { echo echo "$PROGNAME: result => $RESULT" echo } # check_root_id() # check that the command is called by root # return: exit - the command is not called by root # 0 - ok or clean exit in progress check_root_id() { [[ "$CE_RUNNING" = 0 ]] && return 0 if [ $(id -u) != 0 ]; then echo "$PROGNAME: you must be root in order to use this script" >&2 clean_exit 2 fi return 0 } # check_if_running() # check that this script is not running yet # return: exit - the script is already running # 0 - ok check_if_running() { if [[ -f "/var/run/$PROGNAME.pid" ]]; then if [[ "$(ps -p "$(cat "/var/run/$PROGNAME.pid")" -o 'comm=')" = "$PROGNAME" ]]; then echo "$PROGNAME: $PROGNAME already running" >&2 clean_exit 2 else rm -f "/var/run/$PROGNAME.pid" fi fi echo "$$" >"/var/run/$PROGNAME.pid" [[ "$?" -eq 0 ]] && add_exit_cmd "rm -f '/var/run/$PROGNAME.pid'" return 0 } # check_work_dir # check that the working directory exists and contains the subdirectory # tmpfs with read/write permissions # return: exit - the configuration directory isn't all right # 0 - ok or clean exit in progress check_work_dir() { [[ "$CE_RUNNING" = 0 ]] && return 0 if [[ ! -d "$WORKDIR/tmpfs" || ! -r "$WORKDIR/tmpfs" \ || ! -w "$WORKDIR/tmpfs" ]]; then echo "$PROGNAME: the directory '$WORKDIR/tmpfs' doesn't exist," >&2 echo "$PNSPACES isn't readable or writable" >&2 clean_exit 2 fi return 0 } # check_proc_mounts() # check that the file /proc/mounts is available and that it is readable # return: exit - the file isn't available in the pre, run or post mode # 0 - ok or clean exit in progress check_proc_mounts() { [[ "$CE_RUNNING" = 0 ]] && return 0 if [[ ! -f '/proc/mounts' || ! -r '/proc/mounts' ]]; then echo "$PROGNAME: the file '/proc/mounts' must exist and be readable, is the /proc" >&2 echo "$PNSPACES filesystem mounted?" >&2 clean_exit 2 fi return 0 } # mount_tmpfs() size # mount the tmpfs filesystem to the directory mntdir if not mounted yet, # this filesystem remains read/write even if the tmp directory is already # on a read-only filesystem # return: exit - cannot mount the tmpfs filesystem # 0 - ok or clean exit in progress mount_tmpfs() { [[ "$CE_RUNNING" = 0 ]] && return 0 local size="$1" if ! grep -Eq "^tmpfs[[:blank:]]+$WORKDIR/tmpfs" /proc/mounts; then mount -t tmpfs -o size="$size",mode=755,uid=0,gid=0 \ tmpfs "$WORKDIR/tmpfs" if [[ $? -eq 0 ]]; then add_exit_cmd 'umount_tmpfs' else echo "$PROGNAME: cannot mount the tmpfs filesystem to the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs'" >&2 clean_exit 2 fi fi return 0 } # umount_tmpfs() # unmount the tmpfs filesystem if it is mounted # return: 0 - ok (or not mounted) # 1 - cannot unmount the tmpfs filesystem umount_tmpfs() { if grep -Eq "^tmpfs[[:blank:]]+$WORKDIR/tmpfs" /proc/mounts; then umount "$WORKDIR/tmpfs" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot unmount the tmpfs filesystem from the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs'" >&2 return 1 fi fi return 0 } # read_config_file cfg_file # read the configuration file cfg_file, each line of the configuration file # which is not a comment consists of whitespace separated words, the words # can be optionally enclosed in double quotes and can therefore also contain # whitespace characters - space, tab, double quote and backslash may be part # of a word too if they're escaped by a backslash, lines starting with # # (optional whitespaces may precede #) are comments, currently only # the allhost, dev, dm, mntdir and rootdir lines are supported, they are # used to define the ALLHOST, NOSSHFS, DEV, DM, MNTDIR, ROOTDIR and # DEFERSSDSTART global variables # return: exit - the config file isn't readable or doesn't have correct format # 0 - ok or clean exit in progress read_config_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local cfg_file="$1" cfg_cont local linenum line str fch wrd local ifs local isallhost=1 isnosshfs=1 isdev=1 isdm=1 ismntdir=1 isrootdir=1 local isdeferssdstart=1 # check that the config file exists and is readable, then read it if [[ -f "$cfg_file" && -r "$cfg_file" ]]; then cfg_cont="$(cat "$cfg_file")" else echo "$PROGNAME: cannot read the configuration file '$cfg_file'" >&2 clean_exit 2 fi # remove space, tab and newline from IFS so that the whole line # including the leading whitespace is read by the read built-in ifs="$IFS" IFS='' # read and process all lines from the file cfg_file linenum=0 while read -r line; do linenum="$((linenum+1))" unset args local -a args # remove comments, i.e. lines beginning with #, optional whitespace # may precede the # character str=$(echo "$line" | sed -r 's/^([[:blank:]]*)#.*$/\1/') # read words from the line read and store them into the args array while true; do # remove leading whitespace str=$(echo "$str" | sed -nr 's#^[[:blank:]]*(.*)?$#\1#p') # if there is no first character, i.e. no word, then break fch="${str:0:1}" if [[ -z "$fch" ]]; then break # if the first character isn't ", the word is delimited # by whitespace that is not escaped by \ elif [[ "$fch" != '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\4#p') # if the first character is ", the word is delimited by # double quotes that are not escaped by \ and the quoted words # by whitespace elif [[ "$fch" = '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\4#p') fi # replace all characters escaped by \ in the word read wrd=$(echo "$wrd" | sed -r 's#[\\]([[:blank:]"\\])#\1#g') # store the resulting word into the args array args[${#args[*]}]="$wrd" done # if the line has at least one word, check if it is correct and # assign values to appropriate variables if [[ ${#args[*]} -gt 0 ]]; then case "${args[0]}" in 'allhost') if [[ "$isallhost" = 0 ]]; then echo "$PROGNAME: allhost defined multiple times in the config file" >&2 clean_exit 2 fi isallhost=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi ALLHOST="${args[1]}" ;; 'nosshfs') if [[ "$isnosshfs" = 0 ]]; then echo "$PROGNAME: nosshfs defined multiple times in the config file" >&2 clean_exit 2 fi isnosshfs=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi NOSSHFS="${args[1]}" ;; 'dev') if [[ "$isdev" = 0 ]]; then echo "$PROGNAME: dev defined multiple times in the config file" >&2 clean_exit 2 fi isdev=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi DEV="${args[1]}" ;; 'dm') if [[ "$isdm" = 0 ]]; then echo "$PROGNAME: dm defined multiple times in the config file" >&2 clean_exit 2 fi isdm=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi DM="${args[1]}" ;; 'mntdir') if [[ "$ismntdir" = 0 ]]; then echo "$PROGNAME: mntdir defined multiple times in the config file" >&2 clean_exit 2 fi ismntdir=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi MNTDIR="${args[1]%/}" ;; 'rootdir') if [[ "$isrootdir" = 0 ]]; then echo "$PROGNAME: rootdir defined multiple times in the config file" >&2 clean_exit 2 fi isrootdir=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi ROOTDIR="${args[1]%/}" ;; 'deferssdstart') if [[ "$isdeferssdstart" = 0 ]]; then echo "$PROGNAME: deferssdstart defined multiple times in the config file" >&2 clean_exit 2 fi isdeferssdstart=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi DEFERSSDSTART="${args[1]}" ;; *) echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 ;; esac fi done </dev/null; then echo "$PROGNAME: 'sshfs' binary not found and sshfs not disabled" >&2 echo "$PNSPACES in the config file" >&2 clean_exit 2 fi if ! which fusermount >/dev/null; then echo "$PROGNAME: 'fusermount' binary not found and sshfs not disabled" >&2 echo "$PNSPACES in the config file" >&2 clean_exit 2 fi else if echo "$DEV" | grep -q ':'; then echo "$PROGNAME: sshfs disabled in the config file, but the backup device" >&2 echo "$PNSPACES configured as a sshfs filesystem" >&2 clean_exit 2 fi fi # check that the DEV, MNTDIR and ROOTDIR are defined if [[ -z "$DEV" ]]; then echo "$PROGNAME: backup device must be defined, edit the configuration file" >&2 clean_exit 2 fi if [[ -z "$MNTDIR" || ! -d "$MNTDIR" ]]; then echo "$PROGNAME: mount point of the backup device must be defined and must point" >&2 echo "$PNSPACES to an existing directory, create the directory or edit" >&2 echo "$PNSPACES the configuration file" >&2 clean_exit 2 fi if [[ -z "$ROOTDIR" ]]; then echo "$PROGNAME: directory on the backup filesystem containing the backup" >&2 echo "$PNSPACES directory tree must be defined, edit the configuration file" >&2 clean_exit 2 fi return 0 } # get_read_check_config() conflocal confshared # read local configuration, if ALLHOST is defined then copy shared # configuration file, store it to the tmpfs filesystem (which should already # be mounted) and read it, then reread local configuration file once # again to override shared settings by local ones if specified, finally # check configuration # return: exit - cannot get shared configuration file from the host ALLHOST # specified in the local configuration file, the configuration # files aren't readable or do not have correct format or # the configuration isn't correct # 0 - ok or clean exit in progress get_read_check_config() { [[ "$CE_RUNNING" = 0 ]] && return 0 local conflocal="$1" local confshared="$2" local allhost_v allhost_h read_config_file "$CONFDIR/$conflocal" allhost_v="$(echo "$ALLHOST" | sed -r 's#^([^:]*):.*$#\1#')" allhost_h="$(echo "$ALLHOST" | sed -r 's#^[^:]*:(.*)$#\1#')" if [[ -n "$ALLHOST" ]]; then scp -q "$allhost_h:$CONFDIR/$confshared" "$WORKDIR/tmpfs" if [[ "$?" -ne 0 ]]; then echo "$PROGNAME: cannot get shared configuration file from machine" >&2 echo "$PNSPACES '$allhost_v' and store it to the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs', i.e. to the tmpfs filesystem" >&2 clean_exit 2 fi fi if [[ -n "$ALLHOST" ]]; then read_config_file "$WORKDIR/tmpfs/$confshared" read_config_file "$CONFDIR/$conflocal" fi check_config return 0 } # check_uuid_presence() # check whether the UUID syntax is used in the configuration # return: # 0 - used # 1 - not used check_uuid_presence() { echo "${DEV:0:5}" | grep -qi '^UUID=$' [[ $? -eq 0 ]] && return 0 return 1 } # check_addon_tools() # check if add-on tools that are necessary for proper operation are present # in the system, the specific tools depend on given script arguments # or configuration # return: exit - some of the tools are missing # 0 - ok check_addon_tools() { [[ "$CE_RUNNING" = 0 ]] && return 0 local ec=0 # check that the blkid binary exists if UUID= is used in the configuration if check_uuid_presence; then if ! which blkid >/dev/null; then echo "$PROGNAME: 'blkid' binary not found and UUID= syntax used" >&2 echo "$PNSPACES in the config file" >&2 ec=2 fi fi # check that cryptsetup binary exists if the backup device is encrypted, # i.e. if the dm line is specified in the config file if [[ -n "$DM" ]]; then if ! which cryptsetup >/dev/null; then echo "$PROGNAME: 'cryptsetup' binary not found and the backup device" >&2 echo "$PNSPACES is encrypted according to the config file" >&2 ec=2 fi fi [[ "$ec" -ne 0 ]] && clean_exit "$ec" return 0 } # check_outdir() # check that the output directory exists and contains cache and log # subdirectories and if not then create them, all the directories should be # readable and writable # return: exit - the output directory or its cache and log subdirectories # do not exist and cannot be created, they're not readable # and writable # 0 - ok or clean exit in progress check_outdir() { [[ "$CE_RUNNING" = 0 ]] && return 0 local dir for dir in "$OUTDIR" "$BKPDIR" "$LOGDIR"; do if [[ ! -d "$dir" ]]; then mkdir -p "$dir" if [[ "$?" -ne 0 ]]; then echo "$PROGNAME: cannot create directory '$dir'" >&2 clean_exit 2 fi fi [[ ! -r "$dir" ]] && chmod u+r "$dir" [[ ! -w "$dir" ]] && chmod u+w "$dir" if [[ ! -w "$dir" || ! -r "$dir" ]]; then echo "$PROGNAME: directory '$dir' not readable and writable" >&2 clean_exit 2 fi done return 0 } # create_uuid_file # find all block devices in /dev which have uuid assigned and create # the UUID file in the tmpfs directory which maps uuids to devices # return: exit - cannot create uuid file # 0 - ok or clean exit in progress create_uuid_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local dev local uuid if [[ -e "$WORKDIR/tmpfs/$UUID_FILE" ]]; then rm -fr "$WORKDIR/tmpfs/$UUID_FILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete file '$UUID_FILE'" >&2 echo "$PNSPACES from the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 clean_exit 2 fi fi if check_uuid_presence; then find /dev -type b \ | while read dev; do uuid="$(blkid "$dev" | sed -nr 's#^.*[[:blank:]](PT)?UUID=\"([^\"]*)\".*$#\2#p')" if [[ -n "$uuid" ]]; then echo "$uuid $dev" >>"$WORKDIR/tmpfs/$UUID_FILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create file '$UUID_FILE'" >&2 echo "$PNSPACES in the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 clean_exit 2 fi fi done fi return 0 } # get_uuid_device uuid # print path to the device with the given uuid as specified in the uuid file, # print nothing if no device with uuid found # return: 0 - ok get_uuid_device() { local uuid="$1" grep -i "^$uuid[[:blank:]]" "$WORKDIR/tmpfs/$UUID_FILE" \ | head -1 \ | sed -nr 's#^[^[:blank:]]+[[:blank:]]+([^[:blank:]]+)$#\1#p' return 0 } # mount_backup_fs # mount the backup filesystem which may accessed locally as a standard # block device or remotely as a sshfs filesystem, the local block # device may optionally be encrypted using LUKS # return: 0 - ok # 1 - the remote sshfs backup filesystem not mounted # 2 - the local backup device not found # 3 - the local backup device not decrypted # 4 - the local backup filesystem not mounted mount_backup_fs() { local uuid device dmdev local cryptsetup_ec # mount the backup filesystem remotely via sshfs or locally as a device if echo "$DEV" | grep -q ':'; then # mount the remote backup filesystem via sshfs sshfs -o ssh_command='ssh -A' "$DEV" "$MNTDIR" if [[ $? -eq 0 ]]; then add_exit_cmd umount_backup_fs else echo "$PROGNAME: cannot mount the remote backup filesystem via sshfs" return 1 fi else # find the path to the backup device (can be specified by uuid) if echo "${DEV:0:5}" | grep -qi '^UUID=$'; then uuid="$(echo "$DEV" | sed -r 's#^....=##')" device="$(get_uuid_device "$uuid")" else device="/dev/$DEV" fi if [[ -z "$device" || ! -b "$device" ]]; then echo "$PROGNAME: backup device not found" >&2 return 2 fi # decrypt the backup device if [[ -n "$DM" ]]; then if [[ -n "$TAR_LVM_ONE_PASSWORD" ]]; then echo -n \ | awk 'BEGIN {printf("%s\n", ENVIRON["TAR_LVM_ONE_PASSWORD"]);}' \ | cryptsetup luksOpen "$device" "$DM" cryptsetup_ec="$?" else cryptsetup luksOpen "$device" "$DM" cryptsetup_ec="$?" fi if [[ "$cryptsetup_ec" -ne 0 ]]; then echo "$PROGNAME: cannot decrypt the backup device" >&2 return 3 fi fi # mount the local backup filesystem as a device if [[ -n "$DM" ]]; then dmdev="/dev/mapper/$DM" else dmdev="$device" fi mount "$dmdev" "$MNTDIR" if [[ $? -eq 0 ]]; then add_exit_cmd umount_backup_fs else echo "$PROGNAME: cannot mount the backup filesystem" >&2 [[ -n "$DM" ]] && umount_backup_fs 'close' return 4 fi fi # return return 0 } # umount_backup_fs [close_only] # unmount the backup filesystem which may be remotely mounted via sshfs # or just locally as a standard device, the local backup device # is closed as well if encrypted or optionally just closed (not unmounted) # if encrypted and close_only is not empty (the close_only argument # has no effect if backup device is configured as sshfs target) # return: 0 - ok # 1 - the remote backup filesystem not unmounted via sshfs # 2 - the local backup filesystem not unmounted properly # 3 - the encrypted local backup device not closed umount_backup_fs() { local close_only="$1" # unmount the backup filesystem if echo "$DEV" | grep -q ':'; then # unmount the remote backup filesystem if grep -Eq "^[^[:blank:]]+[[:blank:]]+$MNTDIR" /proc/mounts; then fusermount -u "$MNTDIR" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot unmount the remote backup filesystem via sshfs" return 1 fi fi else # unmount the local backup filesystem if [[ -z "$close_only" ]]; then if grep -Eq "^[^[:blank:]]+[[:blank:]]+$MNTDIR" /proc/mounts; then umount "$MNTDIR" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot unmount the backup filesystem" >&2 return 1 fi fi fi # close the unencrypted device if [[ -n "$DM" ]]; then if [[ -b "/dev/mapper/$DM" ]]; then cryptsetup luksClose "$DM" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot close the device mapper backup filesystem" >&2 return 2 fi fi fi fi # return return 0 } # check() # check that the backup of the specified level is allright, i.e. that # all tar-lvm logs of this or lower level exist, that the result # in these logs is OK and that the timestamps in tar-lvm pre, run and post # logs are increasing for each level, the same check is done # for the tar-lvm-one logs # return: 0 - ok or clean exit in progress # 1 - some of the backups isn't ok check() { [[ "$CE_RUNNING" = 0 ]] && return 0 local ec=0 local i mode_log mode log ts local ts_pre1 ts_pre2 ts_run1 ts_run2 ts_post1 ts_post2 local tl2ts_pre tl2ts_run tl2ts_post local allrun log_run log_all # run tar-lvm check for the specified level tar-lvm check "$ARG_LEVEL" "$OUTDIR" [[ $? -ne 0 ]] && ec=1 # check all levels lower or equal to the specified level for ((i=0; i<=ARG_LEVEL; i++)); do # check that tar-lvm logs exist, their results and timestamps tl2ts_pre=1 tl2ts_run=1 tl2ts_post=1 for mode_log in "pre:$LOGDIR/tar-lvm_pre.$i.log" "run:$LOGDIR/tar-lvm_run.$i.log" "post:$LOGDIR/tar-lvm_post.$i.log"; do mode="$(echo "$mode_log" | cut -d: -f1)" log="$(echo "$mode_log" | cut -d: -f2-)" if [[ ! -f "$log" || ! -r "$log" ]]; then echo "$PROGNAME: 'tar-lvm $mode $i' log for level '$i' missing or not readable," >&2 echo "$PNSPACES no '$log'" >&2 ec=1 else grep -Eq '^tar-lvm:[[:blank:]]+result[[:blank:]]*=>[[:blank:]]*OK[[:blank:]]*$' "$log" if [[ $? -ne 0 ]]; then echo "$PROGNAME: 'tar-lvm $mode $i' failed, see log" >&2 echo "$PNSPACES '$log'" >&2 ec=1 else ts="$(sed -rn 's#^tar-lvm:([[:blank:]]+[[:digit:]]+-[[:digit:]]+-[[:digit:]]+[[:blank:]]+[[:digit:]]+:[[:digit:]]+:[[:digit:]]+.*)$#\1#p' "$log")" eval tl2ts_$mode=0 if [[ "$(echo "$ts" | wc -l)" -ne 2 ]]; then echo "$PROGNAME: 'tar-lvm $mode $i' log doesn't contain two timestamps," >&2 echo "$PNSPACES i.e. the start and stop timestamp, see the log" >&2 echo "$PNSPACES '$log'" >&2 ec=1 eval tl2ts_$mode=1 fi case "$mode" in 'pre') if [[ "$tl2ts_pre" = 0 ]]; then ts_pre1="$(date -d "$(echo "$ts" | head -1)" '+%s')" ts_pre2="$(date -d "$(echo "$ts" | tail -1)" '+%s')" fi ;; 'run') if [[ "$tl2ts_run" = 0 ]]; then ts_run1="$(date -d "$(echo "$ts" | head -1)" '+%s')" ts_run2="$(date -d "$(echo "$ts" | tail -1)" '+%s')" fi ;; 'post') if [[ "$tl2ts_post" = 0 ]]; then ts_post1="$(date -d "$(echo "$ts" | head -1)" '+%s')" ts_post2="$(date -d "$(echo "$ts" | tail -1)" '+%s')" fi ;; esac fi fi done # check timestamps in the tar-lvm logs if [[ "$tl2ts_pre" = 0 && "$ts_pre1" -gt "$ts_pre2" ]]; then echo "$PROGNAME: 'tar-lvm pre $i' start time is greater than its stop time" >&2 ec=1 fi if [[ "$tl2ts_pre" = 0 && "$tl2ts_run" = 0 && "$ts_pre2" -gt "$ts_run1" ]]; then echo "$PROGNAME: 'tar-lvm pre $i' stop time is greater than 'tar-lvm run $i'" >&2 echo "$PNSPACES start time" >&2 ec=1 fi if [[ "$tl2ts_run" = 0 && "$ts_run1" -gt "$ts_run2" ]]; then echo "$PROGNAME: 'tar-lvm run $i' start time is greater than its stop time" ec=1 fi if [[ "$tl2ts_run" = 0 && "$tl2ts_post" = 0 && "$ts_run2" -gt "$ts_post1" ]]; then echo "$PROGNAME: 'tar-lvm run $i' stop time is greater than 'tar-lvm post $i'" >&2 echo "$PNSPACES start time" >&2 ec=1 fi if [[ "$tl2ts_post" = 0 && "$ts_post1" -gt "$ts_post2" ]]; then echo "$PROGNAME: 'tar-lvm post $i' start time is greater than its stop time" >&2 ec=1 fi # check that tar-lvm-one logs exist, their results and timestamps, # firstly all or run logs (only one of them can exist) tl2ts_pre=1 tl2ts_run=1 tl2ts_post=1 log_all="$LOGDIR/${PROGNAME}_all.$i.log" log_run="$LOGDIR/${PROGNAME}_run.$i.log" allrun='all' && mode="$allrun" && log="$log_all" [[ ! -f "$log" || ! -r "$log" ]] \ && allrun='run' && mode="$allrun" && log="$log_run" if [[ ! -f "$log" || ! -r "$log" ]]; then echo "$PROGNAME: both '$PROGNAME all $i' and '$PROGNAME run $i' logs" >&2 echo "$PNSPACES are missing or not readable, neither" >&2 echo "$PNSPACES '$log_all'" >&2 echo "$PNSPACES nor '$log_run'" >&2 else grep -Eq "^$PROGNAME:[[:blank:]]+result[[:blank:]]*=>[[:blank:]]*OK[[:blank:]]*\$" "$log" if [[ $? -ne 0 ]]; then echo "$PROGNAME: '$PROGNAME $mode $i' failed, see log" >&2 echo "$PNSPACES '$log'" >&2 ec=1 else # check timestamps in the tar-lvm-one all/run log ts="$(sed -rn "s#^$PROGNAME:([[:blank:]]+[[:digit:]]+-[[:digit:]]+-[[:digit:]]+[[:blank:]]+[[:digit:]]+:[[:digit:]]+:[[:digit:]]+.*)\$#\1#p" "$log")" tl2ts_run=0 if [[ "$(echo "$ts" | wc -l)" -ne 2 ]]; then echo "$PROGNAME: '$PROGNAME $mode $i' log doesn't contain two timestamps," >&2 echo "$PNSPACES i.e. the start and stop timestamp, see the log" >&2 echo "$PNSPACES '$log'" >&2 ec=1 tl2ts_run=1 fi if [[ "$tl2ts_run" = 0 ]]; then ts_run1="$(date -d "$(echo "$ts" | head -1)" '+%s')" ts_run2="$(date -d "$(echo "$ts" | tail -1)" '+%s')" fi fi # check pre and post logs in the run mode as well if [[ "$allrun" = 'run' ]]; then for mode_log in "pre:$LOGDIR/${PROGNAME}_pre.$i.log" "post:$LOGDIR/${PROGNAME}_post.$i.log"; do mode="$(echo "$mode_log" | cut -d: -f1)" log="$(echo "$mode_log" | cut -d: -f2-)" if [[ ! -f "$log" || ! -r "$log" ]]; then echo "$PROGNAME: '$PROGNAME $mode $i' log for level '$i' missing or not readable," >&2 echo "$PNSPACES no '$log'" >&2 else grep -Eq "^$PROGNAME:[[:blank:]]+result[[:blank:]]*=>[[:blank:]]*OK[[:blank:]]*\$" "$log" if [[ $? -ne 0 ]]; then echo "$PROGNAME: '$PROGNAME $mode $i' failed, see log" >&2 echo "$PNSPACES '$log'" >&2 ec=1 else # check timestamps in the tar-lvm-one pre/post log ts="$(sed -rn "s#^$PROGNAME:([[:blank:]]+[[:digit:]]+-[[:digit:]]+-[[:digit:]]+[[:blank:]]+[[:digit:]]+:[[:digit:]]+:[[:digit:]]+.*)\$#\1#p" "$log")" eval tl2ts_$mode=0 if [[ "$(echo "$ts" | wc -l)" -ne 2 ]]; then echo "$PROGNAME: '$PROGNAME $mode $i' log doesn't contain two timestamps," >&2 echo "$PNSPACES i.e. the start and stop timestamp, see the log" >&2 echo "$PNSPACES '$log'" >&2 ec=1 eval tl2ts_$mode=1 fi case "$mode" in 'pre') if [[ "$tl2ts_pre" = 0 ]]; then ts_pre1="$(date -d "$(echo "$ts" | head -1)" '+%s')" ts_pre2="$(date -d "$(echo "$ts" | tail -1)" '+%s')" fi ;; 'post') if [[ "$tl2ts_post" = 0 ]]; then ts_post1="$(date -d "$(echo "$ts" | head -1)" '+%s')" ts_post2="$(date -d "$(echo "$ts" | tail -1)" '+%s')" fi ;; esac fi fi done fi # check timestamps in the tar-lvm-one logs if [[ "$tl2ts_pre" = 0 && "$ts_pre1" -gt "$ts_pre2" ]]; then echo "$PROGNAME: '$PROGNAME pre $i' start time is greater than its stop time" >&2 ec=1 fi if [[ "$tl2ts_pre" = 0 && "$tl2ts_run" = 0 && "$ts_pre2" -gt "$ts_run1" ]]; then echo "$PROGNAME: '$PROGNAME pre $i' stop time is greater than '$PROGNAME run $i'" >&2 echo "$PNSPACES start time" >&2 ec=1 fi if [[ "$tl2ts_run" = 0 && "$ts_run1" -gt "$ts_run2" ]]; then echo "$PROGNAME: '$PROGNAME run $i' start time is greater than its stop time" >&2 ec=1 fi if [[ "$tl2ts_run" = 0 && "$tl2ts_post" = 0 && "$ts_run2" -gt "$ts_post1" ]]; then echo "$PROGNAME: '$PROGNAME run $i' stop time is greater than '$PROGNAME post $i'" >&2 echo "$PNSPACES start time" >&2 ec=1 fi if [[ "$tl2ts_post" = 0 && "$ts_post1" -gt "$ts_post2" ]]; then echo "$PROGNAME: '$PROGNAME post $i' start time is greater than its stop time" >&2 ec=1 fi fi done return "$ec" } # get_timestamp # print timestamp in the RFC-3339 format # return: # 0 - ok get_timestamp() { echo -n "$PROGNAME: " date --rfc-3339=seconds return 0 } # check_subsequent_backups() # check that no backups of the same or higher level than the current level # exist, if so either exit (default) or delete those backups (-f switch # given) # return: exit - some backups of the same or higher level exist (default) # or cannot be deleted (-f switch given) # 0 - ok or clean exit in progress check_subsequent_backups() { [[ "$CE_RUNNING" = 0 ]] && return 0 local f flevel local fstrun=0 delnum [[ "$ARG_F" = 'y' ]] \ && echo "$PROGNAME: deleting the following backup archives" delnum=0 for f in \ $(ls -1 "$BKPDIR" \ | grep -E '^.*[.][0-9]+[.](tgz|snar)$') do flevel=$(echo "$f" | sed -nr 's#^.*[.]([0-9]+)[.][a-z]+$#\1#p') if [[ "$flevel" -ge "$ARG_LEVEL" ]]; then if [[ "$ARG_F" != 'y' ]]; then if [[ "$fstrun" = 0 ]]; then echo "$PROGNAME: the following backups of the same or higher level than $ARG_LEVEL already" >&2 echo "$PNSPACES exist in the directory '$BKPDIR'," echo "$PNSPACES either increase the backup level or use the -f switch to delete" >&2 echo "$PNSPACES those archives" >&2 fstrun=1 fi echo " '$f'" >&2 else echo " '$f'" rm -f "$BKPDIR/$f" if [[ "$?" -eq 0 ]]; then delnum="$((delnum+1))" else echo "$PROGNAME: cannot delete '$BKPDIR/$f'" >&2 fstrun=1 fi fi fi done [[ "$ARG_F" = 'y' ]] \ && echo "$PROGNAME: $delnum backup archive(s) deleted" [[ "$fstrun" != 0 ]] && clean_exit 4 return 0 } # delete_subsequent_logs() [errfd] # delete the logs of the same or higher level than the current level # depending on the specified mode, delete just the logs created directly # by tar-lvm-one, log potential errors that do not belong to the log file # to the errfd file descriptor (default 2) # return: exit - some logs of the same or higher level than the current level # cannot be deleted # 0 - ok or clean exit in progress delete_subsequent_logs() { [[ "$CE_RUNNING" = 0 ]] && return 0 local errfd="${1:-2}" local f flevel local cntrun=0 delnum local nameorlst pre_nameorlst echo "$PROGNAME: deleting the following logs" nameorlst="${PROGNAME}_all|${PROGNAME}_pre|${PROGNAME}_run|${PROGNAME}_post|ssd-backup_stop|tar-lvm_pre|ssd-backup_start|tar-lvm_run|tar-lvm_post|df|tar[.].*" pre_nameorlst="${PROGNAME}_pre|ssd-backup_stop|tar-lvm_pre|ssd-backup_start" case "$ARG_MODE" in 'pre') nameorlst="$pre_nameorlst" ;; 'post') nameorlst="${PROGNAME}_post|tar-lvm_post" ;; esac delnum=0 for f in \ $(ls -1 "$LOGDIR" \ | grep -E '^('"$nameorlst"')[.][0-9]+[.]log$') do flevel=$(echo "$f" | sed -nr 's#^.*[.]([0-9]+)[.][a-z]+$#\1#p') if [[ "$flevel" -ge "$ARG_LEVEL" ]]; then echo "$f" | grep -q "^${PROGNAME}_$ARG_MODE" \ && [[ "$flevel" -eq "$ARG_LEVEL" ]] \ && continue if [[ "$ARG_MODE" = 'run' ]]; then echo "$f" | grep -Eq '^('"$pre_nameorlst"')[.][0-9]+[.]log$' \ && continue fi echo " '$f'" rm -f "$LOGDIR/$f" if [[ "$?" -eq 0 ]]; then delnum="$((delnum+1))" else echo "$PROGNAME: cannot delete '$LOGDIR/$f'" >&"$errfd" cntrun=1 fi fi done echo "$PROGNAME: $delnum log(s) deleted" [[ "$cntrun" != 0 ]] && clean_exit 4 return 0 } # pre() [errfd] # stop all specified processes, remount all filesystems to backup read-only, # create lvm snaphots, remount lvm filesystems read/write, start all # specified processes if not deferred, log potential errors that do not # belong to the log file to the errfd file descriptor (default 2) # return: 0 - ok # 1 - cannot stop processes, remount or create lvm snapshots or start # processes pre() { local errfd="${1:-2}" # failure flag, either empty, STOP, PRE or START local fail='' local nologin='' local ec=0 # check if /var/run/nologin exists [[ -f '/var/run/nologin' ]] && nologin='y' # stop daemons and some processes before the backup ssd-backup -u -v stop >"$LOGDIR/ssd-backup_stop.$ARG_LEVEL.log" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: ssd-backup failed during the stop phase," >&"$errfd" echo "$PNSPACES not all required processes stopped" >&"$errfd" #fail='STOP' fi # create lvm snaphots and remount read/write non-lvm filesystems read-only if [[ -z "$fail" ]]; then tar-lvm -v pre >"$LOGDIR/tar-lvm_pre.$ARG_LEVEL.log" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: tar-lvm failed during the pre phase" >&"$errfd" fail='PRE' fi fi # start daemons if not deferred if [[ "$DEFERSSDSTART" != 'true' ]]; then ssd-backup -u -v start >"$LOGDIR/ssd-backup_start.$ARG_LEVEL.log" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: ssd-backup failed during the start phase," >&"$errfd" echo "$PNSPACES not all required processes started" >&"$errfd" #fail='START' fi fi # restore /var/run/nologin if it was originally present [[ -n "$nologin" ]] && touch '/var/run/nologin' # return [[ -n "$fail" ]] && ec=1 return "$ec" } # run() [errfd] # run the backup by invoking tar-lvm, log potential errors that do not # belong to the log file to the errfd file descriptor (default 2) # return: 0 - ok # 1 - cannot find backup device, decrypt, mount, backup, unmount # or encrypt run() { local errfd="${1:-2}" # failure flag, either empty or BKP local fail='' local ec=0 local tar_lvm_f='' # run the backup if [[ -z "$fail" ]]; then [[ "$ARG_F" = 'y' ]] && tar_lvm_f='-f' tar-lvm -v $tar_lvm_f run "$ARG_LEVEL" "$OUTDIR" >"$LOGDIR/tar-lvm_run.$ARG_LEVEL.log" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: tar-lvm failed during the run phase" >&"$errfd" fail='BKP' fi fi # return [[ -n "$fail" ]] && ec=1 return "$ec" } # post() [errfd] # remove lvm snaphots created during the pre phase and remount those non-lvm # filesystems that were originally mounted read/write back read-write and # start all specified processed it their start was deferred, # log potential errors that do not belong to the log file to the errfd file # descriptor (default 2) # return: 0 - ok # 1 - cannot remove lvm snapshots or remount non-lvm filesystems back # read/write post() { local errfd="${1:-2}" # failure flag, either empty or POST local fail='' local ec=0 # remove the lvm snapshots and remount non-lvm filesystems back read-write # (those that were originally mounted read/write) tar-lvm -v post >"$LOGDIR/tar-lvm_post.$ARG_LEVEL.log" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: tar-lvm failed during the post phase" >&"$errfd" fail='POST' fi # start daemons if deferred if [[ "$DEFERSSDSTART" = 'true' ]]; then ssd-backup -u -v start >"$LOGDIR/ssd-backup_start.$ARG_LEVEL.log" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: ssd-backup failed during the start phase," >&"$errfd" echo "$PNSPACES not all required processes started" >&"$errfd" #fail='START' fi fi # return [[ -n "$fail" ]] && ec=1 return "$ec" } # all() [errfd] # run pre, run and post actions in this order, log potential errors that # do not belong to the log file to the errfd file descriptor (default 2) # return: 0 - ok # 1 - pre, run or post action failed all() { local errfd="${1:-2}" # failure flag, either empty, PRE, RUN or POST local fail='' local ec=0 # pre pre "$errfd" [[ $? -ne 0 ]] && fail='PRE' # run if [[ -z "$fail" ]]; then run "$errfd" [[ $? -ne 0 ]] && fail='RUN' fi # post post "$errfd" [[ $? -ne 0 ]] && fail='POST' # return [[ -n "$fail" ]] && ec=1 return "$ec" } # clean() # perform cleanup, i.e. run tar-lvm clean and unmount tmpfs filesystem # if mounted # return: 0 - ok # 1 - tar-lvm, ssd-backup, or umount of backup fs or tmpfs failed clean() { local ec=0 # run tar-lvm cleanup at first tar-lvm -v clean [[ "$?" -ne 0 ]] && ec=1 # then continue with ssd-backup clean ssd-backup -v clean [[ "$?" -ne 0 ]] && ec=1 # try to unmount the backup filesystem echo "$PROGNAME: unmounting backup filesystem" umount_backup_fs [[ "$?" -ne 0 ]] && ec=1 # unmount tmpfs filesystem if mounted echo "$PROGNAME: unmounting tmpfs filesystem" umount_tmpfs [[ "$?" -ne 0 ]] && ec=1 return "$ec" } ###### MAIN ################################################################### ### check tools, parse and check arguments, run initialization # check if all the external tools are available on the system check_tools grep id sed ps cat rm mount umount true scp mkdir find \ head awk tar-lvm cut wc date tail ls ssd-backup touch # parse and check arguments parse_args "$@" check_args [[ "$ARG_MODE" = 'help' ]] && usage # check if the script is called by root check_root_id # check if the script is not running yet check_if_running # check working directory check_work_dir # check that readable /proc/mounts exists check_proc_mounts # mount the tmpfs filesystem which is always read/write mount_tmpfs "$TMPFS_SIZE" # get shared configuration file if requested, read the configuration files # and check configuration get_read_check_config "$CONFFILE_LOCAL" "$CONFFILE_SHARED" OUTDIR="$MNTDIR/$ROOTDIR/$(hostname -s)" LOGDIR="$OUTDIR/log" BKPDIR="$OUTDIR/cache" # perform cleanup if requested if [[ "$ARG_MODE" = 'clean' ]]; then clean EC="$?" clean_exit "$EC" fi # create file which maps uuids to device names create_uuid_file # check add-on tools presence check_addon_tools # mount backup filesystem mount_backup_fs mount_backup_fs_ec="$?" [[ "$mount_backup_fs_ec" -ne 0 ]] && clean_exit 3 # check output directory check_outdir # perform check if requested if [[ "$ARG_MODE" = 'check' ]]; then check EC="$?" clean_exit "$EC" fi # delete main log file rm -f "$LOGDIR/${PROGNAME}_$ARG_MODE.$ARG_LEVEL.log" if [[ "$?" -ne 0 ]]; then echo "$PROGNAME: cannot delete '$LOGDIR/${PROGNAME}_$ARG_MODE.$ARG_LEVEL.log'" >&2 clean_exit 4 fi # main functionality exec 3>&2 ( # delete items from the exit commands array in this subshell, i.e. # perform cleanup just for the exit commands added in this subshell unset EXIT_CMDS # print timestamp(s) get_timestamp add_exit_cmd 'print_result' add_exit_cmd 'get_timestamp' # check that no backups of the same or higher level exist in the run # or all mode and optionally delete them [[ "$ARG_MODE" = 'run' || "$ARG_MODE" = 'all' ]] \ && check_subsequent_backups # delete the backup logs of the same or higher level than ARG_LEVEL delete_subsequent_logs 3 # either pre, run or post phase or all (pre, run and post in this order) "$ARG_MODE" 3 [[ "$?" -ne 0 ]] && clean_exit 4 # invoke all exit commands added in this subshell RESULT='OK' clean_exit 0 ) >"$LOGDIR/${PROGNAME}_$ARG_MODE.$ARG_LEVEL.log" 2>&1 EC="$?" exec 3>&- ### invoke all remaining exit commands and exit if [[ "$EC" != 0 ]]; then echo "$PROGNAME: $PROGNAME error occured, see log" >&2 echo "$PNSPACES '$LOGDIR/${PROGNAME}_$ARG_MODE.$ARG_LEVEL.log'" >&2 fi [[ "$EC" = 0 ]] && RESULT='OK' clean_exit "$EC" tar-lvm/install/sbin/cmdsend0000755002342000234200000002600414041736622015345 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: cmdsend # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### PROGNAME='cmdsend' PNSPACES=' ' # set versions SCRIPT_VERSION='00.20' SUITE_VERSION='00.80' # set directories and names TMPDIR="/tmp/$PROGNAME" TMPDIR_PID="$TMPDIR/p$$" OUTFILE="$TMPDIR_PID/$PROGNAME.$$.out" LOGFILE="$TMPDIR_PID/$PROGNAME.$$.log" TMPDIR_TIME="$TMPDIR/t$(date '+%Y-%m-%d_%H-%M-%S')" TMPDIR_KEEPPERIOD=14 # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN are available on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 1 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 1 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - run process and send its output by mail" echo echo " ($PROGNAME version: $SCRIPT_VERSION, tar-lvm suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h" echo " $PROGNAME [ [[FROM-ADDR] smtp[-tls|s]://[USER:PASS@]SERVER[:PORT]] \\" echo " $PNSPACES TO-ADDR-1 ... ] -- COMMAND [ARGUMENTS]" echo exit "$ec" } # parse_args() arg1 ... argN # parse the command-line arguments and set the global variables FROM_ADDR, # SMTP_SERVER, SMTP_PORT, TO_ADDRS and COMMAND # return: exit - wrong arguments are given # 0 - ok parse_args() { local arg local hyphens='' unset FROM_ADDR unset SMTP_PROTO unset SMTP_SERVER unset SMTP_PORT unset SMTP_USER unset SMTP_PASS unset TO_ADDRS unset COMMAND [[ $# -eq 1 && "$1" = '-h' ]] && usage [[ $# -lt 2 ]] && usage 1 arg='' if [[ "$1" != '--' ]]; then if echo "$1" | grep -q '://'; then arg="$1" shift elif echo "$2" | grep -q '://'; then FROM_ADDR="$1" arg="$2" shift 2 fi fi if [[ -n "$arg" ]]; then SMTP_SERVER="$(echo "$arg" | sed -rn 's#^smtp(-tls|s)?://([^:]+:[^@]+@)?([[:alnum:].]+)(:[[:digit:]]+)?$#\3#p')" SMTP_PORT="$(echo "$arg" | sed -rn 's#^smtp(-tls|s)?://([^:]+:[^@]+@)?[[:alnum:].]+(:([[:digit:]]+))?$#\4#p')" SMTP_PROTO="$(echo "$arg" | sed -rn 's#^(smtp(-tls|s)?)://([^:]+:[^@]+@)?[[:alnum:].]+(:[[:digit:]]+)?$#\1#p')" SMTP_USER="$(echo "$arg" | sed -rn 's#^smtp(-tls|s)?://(([^:]+):[^@]+@)?[[:alnum:].]+(:[[:digit:]]+)?$#\3#p')" SMTP_PASS="$(echo "$arg" | sed -rn 's#^smtp(-tls|s)?://([^:]+:([^@]+)@)?[[:alnum:].]+(:[[:digit:]]+)?$#\3#p')" [[ -z "$SMTP_SERVER" ]] && usage 1 [[ ( -z "$SMTP_USER" && -n "$SMTP_PASS" ) \ || ( -n "$SMTP_USER" && -z "$SMTP_PASS" ) ]] && usage 1 if [[ -z "$SMTP_PORT" ]]; then if [[ "$SMTP_PROTO" = 'smtps' ]]; then SMTP_PORT=465 elif [[ "$SMTP_PROTO" = 'smtp-tls' ]]; then SMTP_PORT=587 else SMTP_PORT=25 fi fi fi for arg in "$@"; do if [[ -z "$hyphens" && "$arg" = '--' ]]; then hyphens='y' continue fi if [[ -z "$hyphens" ]]; then echo "$arg" | grep -q '^[[:blank:]]*$' && usage 1 TO_ADDRS[${#TO_ADDRS[@]}]="$arg" else [[ ${#COMMAND[@]} -eq 0 ]] \ && echo "$arg" | grep -q '^[[:blank:]]*$' \ && usage 1 COMMAND[${#COMMAND[@]}]="$arg" fi done [[ -z "$hyphens" ]] && usage 1 [[ ${#COMMAND[@]} -eq 0 ]] && usage 1 [[ ${#TO_ADDRS[@]} -eq 0 && -n "$SMTP_SERVER" ]] && usage 1 [[ -z "$SMTP_SERVER" && -n "$FROM_ADDR" ]] && usage 1 return 0 } # check_addon_tools() # check if add-on tools that are necessary for proper operation are present # in the system, the specific tools depend on given script arguments # or configuration # return: exit - some of the tools are missing # 0 - ok check_addon_tools() { local ec=0 unset MAIL_COMMAND if [[ -n "$SMTP_SERVER" ]]; then if [[ "$SMTP_PROTO" = 'smtps' || "$SMTP_PROTO" = 'smtp-tls' ]]; then if ! which openssl >/dev/null 2>&1; then echo "$PROGNAME: 'openssl' binary not found and SMTPS/SMTP-TLS server specified" >&2 ec=1 fi else if ! which telnet >/dev/null 2>&1; then echo "$PROGNAME: 'telnet' binary not found and SMTP server spec ified" >&2 ec=1 fi fi if [[ -n "$SMTP_USER" ]]; then if ! which base64 >/dev/null 2>&1; then echo "$PROGNAME: 'base64' binary not found and SMTP user specified" >&2 ec=1 fi fi else if which mailx >/dev/null 2>&1; then MAIL_COMMAND='mailx' elif which mail >/dev/null 2>&1; then MAIL_COMMAND='mail' else echo "$PROGNAME: neither 'mailx' nor 'mail' binary found and" >&2 echo "$PNSPACES SMTP server not specified" >&2 ec=1 fi fi [[ "$ec" -ne 0 ]] && exit "$ec" return 0 } # delete_piddirs() # delete all stale temporary piddirs/timestamped directories (timestamped # dirs are piddirs whose scripts have finished properly) # return: 0 - ok delete_piddirs() { local pidf pid local tsf tsd tst staledate tsdate # delete all stale pid directories whose processes aren't running anymore ls -1 "$TMPDIR" | grep '^p' | \ while read pidf; do pid="$(echo "$pidf" | sed -rn 's#^p([[:digit:]]+)$#\1#p')" if [[ -n "$pid" ]]; then [[ "$(ps -p "$pid" -o 'comm=')" = "$PROGNAME" ]] && continue rm -fr "$TMPDIR/$pidf" fi done # delete all stale timestamped directories ls -1 "$TMPDIR" | grep '^t' | \ while read tsf; do tsd="$(echo "$tsf" | sed -rn 's#^t([[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2})_.*$#\1#p')" tst="$(echo "$tsf" | sed -rn 's#^t.*_([[:digit:]]{2}-[[:digit:]]{2}-[[:digit:]]{2})$#\1#p' | tr '-' ':')" if [[ -n "$tsd" && -n "$tst" ]]; then staledate="$(date -d "$TMPDIR_KEEPPERIOD days ago" '+%s')" tsdate="$(date -d "$tsd $tst" '+%s')" [[ "$tsdate" -lt "$staledate" ]] && rm -fr "$TMPDIR/$tsf" fi done return 0 } # send_mail subject # send mail with given subject to email addresses specified in the TO_ADDRS # array using telnet if SMTP_SERVER is specified or using mail or mailx # command (whichever is available) if no smtp server was specified (and # thus using local MTA), mail body is taken from standard input # return: 0 - ok # 1 - mailx failed (no smtp server specified) # 2 - openssl or telnet failed (smtp server specified) send_mail() { local subject="$1" local from rcpt local ec=0 if [[ -z "$SMTP_SERVER" ]]; then "$MAIL_COMMAND" -s "$subject" "${TO_ADDRS[@]}" [[ $? -ne 0 ]] && ec=1 else from="$(id -un)" [[ -n "$FROM_ADDR" ]] && from="$FROM_ADDR" ( sleep 2 echo "ehlo $(hostname -f)" sleep 1 if [[ -n "$SMTP_USER" ]]; then echo "auth login" sleep 1 echo "$(echo -n "$SMTP_USER" | base64)" sleep 1 echo "$(echo -n "$SMTP_PASS" | base64)" sleep 1 fi echo "mail from: <$from>" sleep 1 for rcpt in "${TO_ADDRS[@]}"; do echo "rcpt to: <$rcpt>" sleep 1 done echo 'data' sleep 1 echo "From: $from" echo "To: $(echo "${TO_ADDRS[@]}" | sed -r 's#[[:blank:]]+# #g' | tr ' ' ',')\r\n" echo "Subject: $subject" echo "Date: $(date --rfc-2822)" echo cat echo -ne '\n.\n' sleep 1 echo 'quit' sleep 2 ) \ | if [[ "$SMTP_PROTO" = 'smtps' ]]; then openssl s_client -quiet -crlf -ign_eof \ -connect "$SMTP_SERVER:$SMTP_PORT" elif [[ "$SMTP_PROTO" = 'smtp-tls' ]]; then openssl s_client -quiet -starttls smtp -crlf -ign_eof \ -connect "$SMTP_SERVER:$SMTP_PORT" else telnet "$SMTP_SERVER" "$SMTP_PORT" fi if grep -q '^[45][0-9][0-9][^0-9]' "$LOGFILE"; then ec=2 fi if [[ "$(grep '^[23][0-9][0-9][^0-9]' "$LOGFILE" | wc -l)" -lt 9 ]]; then ec=2 fi fi return "$ec" } ###### MAIN ################################################################### # check if all the external tools are available on the system check_tools grep sed ls ps rm date cat mkdir awk mv rmdir # parse and check arguments parse_args "$@" # check add-on tools that are dependent on given args check_addon_tools # delete old tmp directory if it exists if [[ -e "$TMPDIR_PID" ]]; then rm -fr "$TMPDIR_PID" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete '$TMPDIR_PID'" >&2 exit 2 fi fi # delete stale pid directories delete_piddirs # create new tmp directory mkdir -p "$TMPDIR_PID" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create directory '$TMPDIR_PID'" >&2 exit 2 fi # redirect stdout and stderr to a file rm -f "$OUTFILE" "$LOGFILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete outdated file '$OUTFILE'" >&2 exit 2 fi exec >"$OUTFILE" 2>&1 if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot redirect output and errors to '$OUTFILE'" >&2 exit 2 fi # run the specified command "${COMMAND[@]}" # redirect stdout and stderr to logfile now exec >"$LOGFILE" 2>&1 # check if the process generated some output or errors, if so send it by mail OUTSIZE="$(ls -l "$OUTFILE" | awk '{print $5;}')" if [[ -r "$OUTFILE" && "$OUTSIZE" != 0 && "${#TO_ADDRS[@]}" != 0 ]]; then (echo "> ${COMMAND[@]}"; cat "$OUTFILE") | send_mail "$PROGNAME at $(hostname -f)" if [[ $? -ne 0 ]]; then # error message printed to OUTFILE again, the original stdout and # stderr file descriptors must be closed so that the invoked command # can be detached properly, otherwise ssh might hang if used # to run the command in question echo "$PROGNAME: error when sending mail with '$COMMAND' output," >&2 echo "$PNSPACES see the output in '$OUTFILE'" >&2 echo "$PNSPACES and log file in '$LOGFILE'" >&2 fi fi # create timestamped directory mkdir -p "$TMPDIR_TIME" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create timestamped directory '$TMPDIR_TIME'" exit 3 fi # move output and log file to the timestamped directory mv "$OUTFILE" "$LOGFILE" "$TMPDIR_TIME" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot move output and log files to the directory" echo "$PNSPACES '$TMPDIR_TIME'" exit 3 fi # remove pid directory, it should be empty now rmdir "$TMPDIR_PID" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot remove directory '$TMPDIR_PID'" exit 3 fi # exit exit 0 tar-lvm/install/sbin/tar-lvm-all0000755002342000234200000013121514041736702016060 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: tar-lvm-all # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### # set program name PROGNAME='tar-lvm-all' PNSPACES=' ' # set versions SCRIPT_VERSION='00.22' SUITE_VERSION='00.80' # set configuration directories and files CONFDIR='/usr/local/etc/tar-lvm' WORKDIR='/var/local/tar-lvm' CONFFILE="$PROGNAME.conf" CONFFILE_ONE_LOCAL='tar-lvm-one.local.conf' CONFFILE_ONE_SHARED='tar-lvm-one.shared.conf' DEVDEFFILE='virtio-pmdev-to-vmdev.xml' # set uuid filename UUID_FILE="uuid.list.$PROGNAME" # set tmpfs size in bytes, period in seconds to check each machine/host # if tar-lvm-one is still running on it, delay period in seconds to wait # after pm backup (so that vms on it get ready) and number of ssh # retries/failures when checking if tar-lvm-one is still running # (i.e. how many consecutive failures cause the host to be considered # inactive/dead) TMPFS_SIZE="$((20*1024*1024))" VMCHECK_PERIOD=10 POSTPM_DELAY=40 SSHCHECK_RETRIES=3 # exit commands array and clean exit running flag unset EXIT_CMDS CE_RUNNING=1 # unset global variables defined after reading the configuration file, i.e. # name of the backup device on the physical and virtual host, passprompt # flag, physical machine backup flag, names of virtual machines/hosts # to backup, hosts to backup, number of hosts to backup in parallel, # mailto, smtpserver and mailfrom values (used to sending output by email) unset PMDEV unset VMDEV unset PASSPROMPT unset PMBACKUP unset VM unset HOST unset PARHOSTNUM unset MAILTO unset SMTPSERVER unset MAILFROM # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN and which are available # on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 1 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 1 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - run tar-lvm-one on some virtual machines on one physical" echo "$PNSPACES machine (physical machine can be included) and on chosen" echo "$PNSPACES hosts via SSH" echo echo " ($PROGNAME version: $SCRIPT_VERSION, tar-lvm suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h | help" echo " $PROGNAME [-v] pre level" echo " $PROGNAME [-v] [-f] run level" echo " $PROGNAME [-v] post level" echo " $PROGNAME [-v] [-f] all level" echo " $PROGNAME check level" echo " $PROGNAME clean" echo echo ' -h ... print help and exit' echo " pre ... prepare all hosts for the backup, i.e. run 'tar-lvm-one pre'" echo " on each host" echo " run ... run the backup on each host, i.e. run 'tar-lvm-one run level'" echo " post ... finalize the backup on each host, i.e. run 'tar-lvm-one post'" echo " on each host" echo " all ... all phases, i.e. run 'tar-lvm-one all level' on each host" echo ' check ... check the results of the backups of the specified level' echo ' and of all preceding levels on all hosts' echo " clean ... perform cleanup on all hosts, i.e. run 'tar-lvm-one clean'" echo ' on each host, then perform local cleanup' echo ' level ... level of the incremental backup, i.e. non-negative integer' echo ' -v ... print verbose info about hosts being processed' echo ' -f ... force the backup, pass this argument to tar-lvm-one' echo exit "$ec" } # parse_args() arg1 ... argN # parse the command-line arguments and set the global variables ARG_MODE, # ARG_LEVEL, ARG_V and ARG_F # return: exit - wrong arguments are given # 0 - ok parse_args() { local arg ARGNUM=0 ARG_MODE='' ARG_LEVEL='' ARG_V='n' ARG_F='n' for arg in "$@"; do case "$arg" in '-v' ) [[ "$ARG_V" = 'y' ]] && usage 1 ARG_V='y' ;; '-f' ) [[ "$ARG_F" = 'y' ]] && usage 1 ARG_F='y' ;; *) if [[ "${arg:0:1}" = '-' \ && ( "$arg" != '-h' || "$ARGNUM" -gt 0 ) ]]; then usage 1 else ARGNUM="$((ARGNUM+1))" case "$ARGNUM" in 1) case "$arg" in '-h' | 'help') ARG_MODE='help' ;; 'pre' | 'run' | 'post' | 'all' | 'clean' | 'check') ARG_MODE="$arg" ;; *) usage 1 ;; esac ;; 2) ARG_LEVEL="$arg" ;; esac fi ;; esac done if [[ -z "$ARG_MODE" ]]; then usage 1 elif [[ "$ARG_MODE" = 'help' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_F" = 'y' || "$ARG_V" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'clean' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_F" = 'y' || "$ARG_V" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'check' ]]; then [[ "$ARGNUM" -ne 2 || "$ARG_F" = 'y' || "$ARG_V" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'pre' || "$ARG_MODE" = 'post' || "$ARG_MODE" = 'run' || "$ARG_MODE" = 'all' ]]; then [[ "$ARGNUM" -ne 2 ]] && usage 1 fi return 0 } # check_args() # check that the command-line arguments are correct, must be called after # parse_args # return: exit - the arguments are not correct # 0 - ok check_args() { if [[ "$ARG_MODE" = 'pre' || "$ARG_MODE" = 'run' || "$ARG_MODE" = 'post' || "$ARG_MODE" = 'all' || "$ARG_MODE" = 'check' ]]; then if ! echo "$ARG_LEVEL" | grep -Eq '^[0-9]+$'; then echo "$PROGNAME: level must be a non-negative integer" >&2 exit 1 fi fi return 0 } # add_exit_cmd exit_cmd # add exit command to be invoked when clean_exit is invoked, the command # is just one argument to be invoked via eval # return: 0 - ok add_exit_cmd() { local cmd="$1" local i i="${#EXIT_CMDS[@]}" EXIT_CMDS[i]="$cmd" return 0; } # del_exit_cmd [exit_cmd_number] # remove exit command to be invoked when clean_exit is invoked, the command # number can be specified, if it is not, the last command is removed if any # return: 0 - ok del_exit_cmd() { local i="$1" local j if [[ "${#EXIT_CMDS[@]}" -gt 0 ]]; then if [[ -z "$i" ]]; then j="$(( ${#EXIT_CMDS[@]} - 1 ))" unset EXIT_CMDS[j] else EXIT_CMDS[i]='' fi fi return 0; } # run_exit_cmds # invoke all registered exit commands in the opposite order in which they # were registered, but do not actually exit, then remove all exit commands # return: 0 - ok # 1 - some exit command failed run_exit_cmds() { local i n local cmd local eval_ec ec=0 n="${#EXIT_CMDS[@]}" for ((i=n-1; i>=0; i--)); do cmd="${EXIT_CMDS[i]}" if [[ -n "$cmd" ]]; then eval "$cmd" eval_ec="$?" fi if [[ "$eval_ec" -ne 0 ]]; then ec=1 fi done unset EXIT_CMDS return "$ec" } # clean_exit [ec] # exit cleanly, i.e. invoke all registered exit commands before exiting # in the opposite order in which they were registered, the exit commands # can be (de)registered by using add_exit_cmd/del_exit_cmd, finally, # exit with the given exit code or with 1 if no exit code is given # and some command fails # return: exit clean_exit() { local ec_orig="${1:-0}" local i n local cmd local ec=0 CE_RUNNING=0 run_exit_cmds [[ "$?" -ne 0 ]] && ec=1 [[ "$ec_orig" = 0 && "$ec" != 0 ]] && ec_orig="$ec" exit "$ec_orig" } # check_root_id() # check that the command is called by root # return: exit - the command is not called by root # 0 - ok or clean exit in progress check_root_id() { [[ "$CE_RUNNING" = 0 ]] && return 0 if [[ $(id -u) != 0 ]]; then echo "$PROGNAME: you must be root in order to use this script" >&2 clean_exit 2 fi return 0 } # check_if_running() # check that this script is not running yet # return: exit - the script is already running # 0 - ok check_if_running() { if [[ -f "/var/run/$PROGNAME.pid" ]]; then if [[ "$(ps -p "$(cat "/var/run/$PROGNAME.pid")" -o 'comm=')" = "$PROGNAME" ]]; then echo "$PROGNAME: $PROGNAME already running" >&2 clean_exit 2 else rm -f "/var/run/$PROGNAME.pid" fi fi echo "$$" >"/var/run/$PROGNAME.pid" [[ "$?" -eq 0 ]] && add_exit_cmd "rm -f '/var/run/$PROGNAME.pid'" return 0 } # check_work_dir # check that the working directory exists and contains the subdirectories # tmp and tmpfs with read/write permissions # return: exit - the configuration directory isn't all right # 0 - ok or clean exit in progress check_work_dir() { [[ "$CE_RUNNING" = 0 ]] && return 0 if [[ ! -d "$WORKDIR/tmp" || ! -r "$WORKDIR/tmp" \ || ! -w "$WORKDIR/tmp" ]]; then echo "$PROGNAME: the directory '$WORKDIR/tmp' doesn't exist," >&2 echo "$PNSPACES isn't readable or writable" >&2 clean_exit 2 fi if [[ ! -d "$WORKDIR/tmpfs" || ! -r "$WORKDIR/tmpfs" \ || ! -w "$WORKDIR/tmpfs" ]]; then echo "$PROGNAME: the directory '$WORKDIR/tmpfs' doesn't exist," >&2 echo "$PNSPACES isn't readable or writable" >&2 clean_exit 2 fi return 0 } # mount_tmpfs() size # mount the tmpfs filesystem to the directory mntdir if not mounted yet, # this filesystem remains read/write even if the tmp directory is already # on a read-only filesystem # return: exit - cannot mount the tmpfs filesystem # 0 - ok or clean exit in progress mount_tmpfs() { [[ "$CE_RUNNING" = 0 ]] && return 0 local size="$1" if ! grep -Eq "^tmpfs[[:blank:]]+$WORKDIR/tmpfs" /proc/mounts; then mount -t tmpfs -o size="$size",mode=755,uid=0,gid=0 \ tmpfs "$WORKDIR/tmpfs" if [[ $? -eq 0 ]]; then add_exit_cmd 'umount_tmpfs' else echo "$PROGNAME: cannot mount the tmpfs filesystem to the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs'" >&2 clean_exit 2 fi fi return 0 } # umount_tmpfs() # unmount the tmpfs filesystem if it is mounted # return: 0 - ok (or not mounted) # 1 - cannot unmount the tmpfs filesystem umount_tmpfs() { if grep -Eq "^tmpfs[[:blank:]]+$WORKDIR/tmpfs" /proc/mounts; then umount "$WORKDIR/tmpfs" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot unmount the tmpfs filesystem from the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs'" >&2 return 1 fi fi return 0 } # read_config_file 'all'|'one.part' cfg_file # read the configuration file cfg_file, each line of the configuration file # which is not a comment consists of whitespace separated words, the words # can be optionally enclosed in double quotes and can therefore also contain # whitespace characters - space, tab, double quote and backslash may be part # of a word too if they're escaped by a backslash, lines starting with # # (optional whitespaces may precede #) are comments, currently only # the pmdev, vmdev, passprompt, pmbackup, vm, host, parhostnum and mail # lines are supported if 'all' is specified, they are used to define the # PMDEV, VMDEV, PASSPROMPT, PMBACKUP, VM, HOST, PARHOSTNUM and MAILTO # global variables, only the allhost and dev lines are supported # if 'one.part' is specified, they're used to define the ALLHOST and DEV # global variables # return: exit - the config file isn't readable or doesn't have correct format # if 'all' is specified # 0 - ok or clean exit in progress # 1 - the config file isn't readable or doesn't have correct format # if 'one.part' is specified read_config_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local mode="$1" local cfg_file="$2" cfg_cont local linenum line str fch wrd local ifs local ispmdev=1 isvmdev=1 ispassprompt=1 ispmbackup=1 isparhostnum=1 local issmtpserver=1 ismailfrom=1 local isallhost=1 isdev=1 local n # check that the config file exists and is readable if [[ "$mode" != 'one.part' ]]; then if [[ -f "$cfg_file" && -r "$cfg_file" ]]; then cfg_cont="$(cat "$cfg_file")" else echo "$PROGNAME: cannot read the configuration file '$cfg_file'" >&2 clean_exit 2 fi else if [[ -f "$cfg_file" && -r "$cfg_file" ]]; then cfg_cont="$(cat "$cfg_file")" else echo "$PROGNAME: cannot read the configuration file '$cfg_file'" >&2 return 1 fi fi # remove space, tab and newline from IFS so that the whole line # including the leading whitespace is read by the read built-in ifs="$IFS" IFS='' # read and process all lines from the file cfg_file linenum=0 while read -r line; do linenum="$((linenum+1))" unset args local -a args # remove comments, i.e. lines beginning with #, optional whitespace # may precede the # character str=$(echo "$line" | sed -r 's/^([[:blank:]]*)#.*$/\1/') # read words from the line read and store them into the args array while true; do # remove leading whitespace str=$(echo "$str" | sed -nr 's#^[[:blank:]]*(.*)?$#\1#p') # if there is no first character, i.e. no word, then break fch="${str:0:1}" if [[ -z "$fch" ]]; then break # if the first character isn't ", the word is delimited # by whitespace that is not escaped by \ elif [[ "$fch" != '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\4#p') # if the first character is ", the word is delimited by # double quotes that are not escaped by \ and the quoted words # by whitespace elif [[ "$fch" = '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\4#p') fi # replace all characters escaped by \ in the word read wrd=$(echo "$wrd" | sed -r 's#[\\]([[:blank:]"\\])#\1#g') # store the resulting word into the args array args[${#args[*]}]="$wrd" done # if the line has at least one word, check if it is correct and # assign values to appropriate variables if [[ ${#args[*]} -gt 0 ]]; then if [[ "$mode" != 'one.part' ]]; then case "${args[0]}" in 'pmdev') if [[ "$ispmdev" = 0 ]]; then echo "$PROGNAME: pmdev defined multiple times in the config file" >&2 clean_exit 2 fi ispmdev=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi PMDEV="${args[1]}" ;; 'vmdev') if [[ "$isvmdev" = 0 ]]; then echo "$PROGNAME: vmdev defined multiple times in the config file" >&2 clean_exit 2 fi isvmdev=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi VMDEV="${args[1]}" ;; 'passprompt') if [[ "$ispassprompt" = 0 ]]; then echo "$PROGNAME: passprompt defined multiple times in the config file" >&2 clean_exit 2 fi ispassprompt=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi PASSPROMPT="${args[1]}" ;; 'pmbackup') if [[ "$ispmbackup" = 0 ]]; then echo "$PROGNAME: pmbackup defined multiple times in the config file" >&2 clean_exit 2 fi ispmbackup=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi PMBACKUP="${args[1]}" ;; 'vm') if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#VM[*]}" VM[n]="${args[1]}" ;; 'host') if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#HOST[*]}" HOST[n]="${args[1]}" ;; 'parhostnum') if [[ "$isparhostnum" = 0 ]]; then echo "$PROGNAME: parhostnum defined multiple times in the config file" >&2 clean_exit 2 fi isparhostnum=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi PARHOSTNUM="${args[1]}" ;; 'mailto') if [[ ${#args[*]} -lt 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#MAILTO[*]}" MAILTO[n]="${args[1]}" ;; 'smtpserver') if [[ "$issmtpserver" = 0 ]]; then echo "$PROGNAME: smtpserver defined multiple times in the config file" >&2 clean_exit 2 fi issmtpserver=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi SMTPSERVER="${args[1]}" ;; 'mailfrom') if [[ "$ismailfrom" = 0 ]]; then echo "$PROGNAME: mailfrom defined multiple times in the config file" >&2 clean_exit 2 fi ismailfrom=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi MAILFROM="${args[1]}" ;; *) echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 ;; esac else case "${args[0]}" in 'allhost') if [[ "$isallhost" = 0 ]]; then echo "$PROGNAME: allhost defined multiple times in the config file" >&2 return 1 fi isallhost=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 return 1 fi ALLHOST="${args[1]}" ;; 'dev') if [[ "$isdev" = 0 ]]; then echo "$PROGNAME: dev defined multiple times in the config file" >&2 return 1 fi isdev=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 return 1 fi DEV="${args[1]}" ;; *) echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 return 1 ;; esac fi fi done <&2 echo "$PNSPACES if at least one virtual machine should be backed up," >&2 echo "$PNSPACES see the 'pmdev' and 'vm' directives in the configuration" >&2 echo "$PNSPACES file" >&2 clean_exit 2 fi if [[ -z "$VMDEV" ]]; then echo "$PROGNAME: backup device on the virtual machines must be defined" >&2 echo "$PNSPACES if at least one virtual machine should be backed up," >&2 echo "$PNSPACES see the 'vmdev' and 'vm' directives in the configuration" >&2 echo "$PNSPACES file" >&2 clean_exit 2 fi fi if [[ -z "$PMBACKUP" && "${#VM[@]}" = 0 && "${#HOST[@]}" = 0 ]]; then echo "$PROGNAME: no host to backup specified in the configuration file," >&2 echo "$PNSPACES see the 'pmbackup', 'vm' and 'host' directives" >&2 clean_exit 2 fi if [[ -z "$PARHOSTNUM" ]] || echo "$PARHOSTNUM" | grep -Evq '^[1-9][0-9]*$'; then echo "$PROGNAME: number of hosts to backup in parallel not specified" >&2 echo "$PNSPACES in the configuration file or not specified" >&2 echo "$PNSPACES as a positive integer, see the 'parhostnum directive" >&2 clean_exit 2 fi if [[ "${#MAILTO[*]}" -eq 0 ]]; then MAILTO[0]='root' fi [[ -z "$SMTPSERVER" ]] && unset MAILFROM else if [[ -z "$DEV" ]]; then if [[ -z "$vmname" ]]; then echo "$PROGNAME: backup device not configured on unknown machine," >&2 else echo "$PROGNAME: backup device not configured on machine '$vmname'," >&2 fi echo -n "$PNSPACES edit its configuration files" >&2 [[ -z "$noendlf" ]] && echo >&2 return 1 fi fi return 0 } # check_uuid_presence() # check whether the UUID syntax is used in the configuration # return: # 0 - used # 1 - not used check_uuid_presence() { echo "${PMDEV:0:5}" | grep -qi '^UUID=$' [[ $? -eq 0 ]] && return 0 return 1 } # check_addon_tools() # check if add-on tools that are necessary for proper operation are present # in the system, the specific tools depend on given script arguments # or configuration # return: exit - some of the tools are missing # 0 - ok check_addon_tools() { [[ "$CE_RUNNING" = 0 ]] && return 0 local ec=0 # check that the blkid binary exists if UUID= is used in the configuration if check_uuid_presence; then if ! which blkid >/dev/null; then echo "$PROGNAME: 'blkid' binary not found and UUID= syntax used" >&2 echo "$PNSPACES in the config file" >&2 ec=2 fi fi # check that virsh binary exists if at least one vm is specified if [[ "${#VM[@]}" -gt 0 ]]; then if ! which virsh >/dev/null; then echo "$PROGNAME: 'virsh' binary not found and at least one virtual" >&2 echo "$PNSPACES machine specified in the config file" >&2 ec=2 fi fi [[ "$ec" -ne 0 ]] && clean_exit "$ec" return 0 } # create_uuid_file # find all block devices in /dev which have uuid assigned and create # file filename in the tmpfs directory which maps uuids to devices # return: exit - cannot create uuid file # 0 - ok or clean exit in progress create_uuid_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local dev local uuid if [[ -e "$WORKDIR/tmpfs/$UUID_FILE" ]]; then rm -fr "$WORKDIR/tmpfs/$UUID_FILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete file '$UUID_FILE'" >&2 echo "$PNSPACES from the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 clean_exit 2 fi fi if check_uuid_presence; then find /dev -type b \ | while read dev; do uuid="$(blkid "$dev" | sed -nr 's#^.*[[:blank:]](PT)?UUID=\"([^\"]*)\".*$#\2#p')" if [[ -n "$uuid" ]]; then echo "$uuid $dev" >>"$WORKDIR/tmpfs/$UUID_FILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create file '$UUID_FILE'" >&2 echo "$PNSPACES in the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 clean_exit 2 fi fi done fi return 0 } # get_uuid_device uuid # print path to the device with the given uuid as specified in the uuid file # uuid_filename, print nothing if no device with uuid found # return: 0 - ok get_uuid_device() { local uuid="$1" grep -i "^$uuid[[:blank:]]" "$WORKDIR/tmpfs/$UUID_FILE" \ | head -1 \ | sed -nr 's#^[^[:blank:]]+[[:blank:]]+([^[:blank:]]+)$#\1#p' return 0 } # create_devdef_file() devicename # create libvirt device definition file to access a device devicename # attached to physical machine from virtual machines # return: exit (and umount tmpfs) - cannot (re)create the devdeffile # 0 - ok or clean exit in progress create_devdef_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local devicename="$1" local uuid device rm -f "$WORKDIR/tmpfs/$DEVDEFFILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete device definition file '$DEVDEFFILE'" >&2 echo "$PNSPACES from the directory '$WORKDIR/tmpfs' on the tmpfs filesystem," >&2 echo "$PNSPACES cannot continue" >&2 clean_exit 2 fi # find the path to the backup device (can be specified by uuid) if echo "${devicename:0:5}" | grep -qi '^UUID=$'; then uuid="$(echo "$devicename" | sed -r 's#^....=##')" device="$(get_uuid_device "$uuid")" else device="/dev/$devicename" fi cat <"$WORKDIR/tmpfs/$DEVDEFFILE" EOF_TARLVMALL_DEVDEF_821 if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create device definition file '$DEVDEFFILE'" >&2 echo "$PNSPACES in the directory '$WORKDIR/tmpfs' on the tmpfs filesystem," >&2 echo "$PNSPACES cannot continue" >&2 clean_exit 2 fi return 0 } # clean_backup_device() dev [vm] # detach backup device dev from virtual machine vm if it is attached # or from any configured vm it is attached to if vm isn't specified, # backup device can be specified as UUID=... or as absolute device path # return: exit - critical error occured, cannot continue # 0 - ok (detached or not found) or clean exit in progress # 1 - error occured, device not detached, but we can still continue # with cleanup clean_backup_device() { local dev="$1" local vm="$2" local uuid local device [[ "$CE_RUNNING" = 0 ]] && return 0 # find out device path and create qemu device definiton file uuid="${dev#UUID=}" [[ "$uuid" = "$dev" ]] && uuid='' if [[ -n "$uuid" ]]; then device="$(get_uuid_device "$uuid")" else device="$dev" fi create_devdef_file "$device" # find out vm to which the backup device is attached to if [[ -z "$vm" ]]; then for vm in "${VM[@]}"; do vm_v="$(echo "$vm" | sed -r 's#^([^:]*):.*$#\1#')" if virsh domblklist "$vm_v" | grep -Eq "^[^[:blank:]]+[[:blank:]]+$device\$"; then break fi vm_v='' done else vm_v="$(echo "$vm" | sed -r 's#^([^:]*):.*$#\1#')" if ! virsh domblklist "$vm_v" | grep -Eq "^[^[:blank:]]+[[:blank:]]+$device\$"; then vm_v='' fi fi # detach device from vm if found if [[ -n "$vm_v" ]]; then virsh detach-device "$vm_v" "$WORKDIR/tmpfs/$DEVDEFFILE" >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot detach backup device from virtual machine '$vm_v'" >&2 return 1 fi fi return 0 } # clean() # perform local cleanup, i.e. detach the backup device from some virtual # machine if attached and unmount tmpfs filesystem if mounted # return: 0 - ok # 1 - backup device not detached or tmpfs filesystem not unmounted clean() { local ec=0 # detach backup device from vm it is attached to clean_backup_device "$PMDEV" [[ "$?" -ne 0 ]] && ec=1 # unmount tmpfs filesystem if mounted umount_tmpfs [[ "$?" -ne 0 ]] && ec=1 return "$ec" } # all() cfgfile_one_local cfgfile_one_shared devpassvar vmperiod # run all actions related to all vms and then hosts to backup, i.e. get # the needed lines from the local and shared configuration file # for tar-lvm-one for each vm/host, read this configuration, attach # the backup device using devdeffile if backing up a vm (not just host), # run tar-lvm-one via bgrun in the requested mode on the host/vm and pass # it the password contained in the exported variable devpassvar # (the password can be empty so that tar-lvm-one asks for one), wait until # tar-lvm-one is done on the vm/host, then detach the backup device # from the vm, vms are backed up serially, but hosts in parallel, # maximal number of hosts to back up in parallel is specified # in the PARHOSTNUM configuration variable, cleanup or check can be # optionally performed on all machines in the clean or check mode instead # return: exit - critical error occured, cannot continue # 0 - ok or clean exit in progress # 1 - error occured, but we can still backup next vm host all() { [[ "$CE_RUNNING" = 0 ]] && return 0 local cfgfile_one_local="$1" local cfgfile_one_shared="$2" local devpassvar="$3" local hostperiod="$4" local ec=0 result='OK' local i vmstr vmhost ispm=0 isvm=0 parvmhostnum=1 parvmhosts='' local vh_fail vh fail fail_now local stmsg # unset tar-lvm-one config variables unset ALLHOST unset DEV # process all pm/vms/hosts and add a dummy host to wait # for all to finish vmstr="$vmstr /" for vmhost in localhost "${VM[@]}" : "${HOST[@]}" $vmstr; do vmhost_v="$(echo "$vmhost" | sed -r 's#^([^:]*):.*$#\1#')" vmhost_h="$(echo "$vmhost" | sed -r 's#^[^:]*:(.*)$#\1#')" # skip most actions except waiting for parallel backups to finish # if processing just dummy host entry if [[ "$vmhost" != '/' ]]; then # if this is the first iteration and physical machine shouldn't be # backed up, skip this iteration if [[ -n "$ispm" && "$PMBACKUP" != 'true' ]]; then unset ispm continue fi # delimiter ':' instead of vm/host changes the type of vm/host # from vm to host if [[ "$vmhost" = ':' ]]; then if [[ "$ARG_MODE" != 'clean' && "$ARG_MODE" != 'check' ]]; then parvmhostnum="$PARHOSTNUM" fi unset isvm continue fi # print status info in the clean and check mode if [[ "$ARG_MODE" = 'clean' ]]; then echo "$PROGNAME: starting cleanup on host '$vmhost_v'" elif [[ "$ARG_MODE" = 'check' ]]; then echo "$PROGNAME: starting check on host '$vmhost_v'" fi # get local tar-lvm-one config file from the vm or host, we need # allhost and dev lines, then read it to get the allhost if [[ "$ARG_MODE" != 'clean' ]]; then if [[ -e "$WORKDIR/tmpfs/$cfgfile_one_local.part" ]]; then rm -fr "$WORKDIR/tmpfs/$cfgfile_one_local.part" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete temporary file '$cfgfile_one_local.part'" >&2 echo "$PNSPACES from the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 echo "$PNSPACES before recreating new one for host '$vmhost_v'," >&2 echo "$PNSPACES skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi fi ssh -x -A "root@$vmhost_h" "[ -r '$CONFDIR/$cfgfile_one_local' ] && ( grep -E '^[[:blank:]]*(allhost|dev)([[:blank:]]+.*)?$' '$CONFDIR/$cfgfile_one_local' || true )" >"$WORKDIR/tmpfs/$cfgfile_one_local.part" 2>"$WORKDIR/tmpfs/stderr.tmp" if [[ $? -ne 0 ]]; then grep -Ev '^stdin: is not a tty$' "$WORKDIR/tmpfs/stderr.tmp" >&2 echo "$PROGNAME: cannot create temporary file '$cfgfile_one_local.part'" >&2 echo "$PNSPACES in the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 echo "$PNSPACES for host '$vmhost_v', skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi read_config_file 'one.part' "$WORKDIR/tmpfs/$cfgfile_one_local.part" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot read config file '$cfgfile_one_local.part'" >&2 echo "$PNSPACES in the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 echo "$PNSPACES for host '$vmhost_v', skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi fi # get shared tar-lvm-one config file from the "all" host if allhost # was specifed in the local config file, we need dev line if [[ "$ARG_MODE" != 'clean' ]]; then if [[ -n "$ALLHOST" ]]; then if [[ -e "$WORKDIR/tmpfs/$cfgfile_one_shared.part" ]]; then rm -fr "$WORKDIR/tmpfs/$cfgfile_one_shared.part" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete temporary file '$cfgfile_one_shared.part'" >&2 echo "$PNSPACES from the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 echo "$PNSPACES before recreating new one for host '$vmhost_v'," >&2 echo "$PNSPACES skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi fi [[ -r "$CONFDIR/$cfgfile_one_shared" ]] && ( grep -E '^[[:blank:]]*(dev)([[:blank:]]+.*)?$' "$CONFDIR/$cfgfile_one_shared" || true ) >"$WORKDIR/tmpfs/$cfgfile_one_shared.part" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create temporary file '$cfgfile_one_shared.part'" >&2 echo "$PNSPACES in the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 echo "$PNSPACES for host '$vmhost_v', skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi fi fi # read the obtained tar-lvm-one config files if [[ "$ARG_MODE" != 'clean' ]]; then if [[ -n "$ALLHOST" ]]; then read_config_file 'one.part' "$WORKDIR/tmpfs/$cfgfile_one_shared.part" read_config_file 'one.part' "$WORKDIR/tmpfs/$cfgfile_one_local.part" fi if ! check_config 'one.part' "$vmhost_v" 'y'; then echo ", skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi fi # attach the pmdev device to vm (not to pm in the first # iteration) if [[ "$ARG_MODE" != 'clean' ]]; then if [[ -n "$isvm" && -z "$ispm" ]]; then virsh attach-device "$vmhost_v" "$WORKDIR/tmpfs/$DEVDEFFILE" >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot attach backup device to virtual machine '$vmhost_v'," >&2 echo "$PNSPACES skipping '$vmhost_v' backup ..." >&2 ec=1 continue fi # give the vm some time to create the device sleep 4 fi fi # run tar-lvm-one via bgrun on each machine unset TAR_LVM_ONE_F [[ "$ARG_F" = 'y' ]] && TAR_LVM_ONE_F='-f' if [[ "$ARG_MODE" = 'clean' ]]; then ssh -x -A "root@$vmhost_h" "tar-lvm-one $ARG_MODE 2>&1" 2>"$WORKDIR/tmpfs/stderr.tmp" if [[ $? -eq 0 ]]; then if [[ -z "$parvmhosts" ]]; then parvmhosts="$vmhost/0" else parvmhosts="$parvmhosts $vmhost/0" fi else grep -Ev '^stdin: is not a tty$' "$WORKDIR/tmpfs/stderr.tmp" >&2 echo "$PROGNAME: tar-lvm-one failed on host '$vmhost_v'" >&2 ec=1 fi elif [[ "$ARG_MODE" = 'check' ]]; then echo -n \ | awk 'BEGIN {printf("%s\n", ENVIRON["'"$devpassvar"'"]);}' \ | ssh -x -A "root@$vmhost_h" "bash -c 'read -s TAR_LVM_ONE_PASSWORD; export TAR_LVM_ONE_PASSWORD; tar-lvm-one $ARG_MODE $ARG_LEVEL 2>&1'" 2>"$WORKDIR/tmpfs/stderr.tmp" if [[ $? -eq 0 ]]; then if [[ -z "$parvmhosts" ]]; then parvmhosts="$vmhost/0" else parvmhosts="$parvmhosts $vmhost/0" fi else grep -Ev '^stdin: is not a tty$' "$WORKDIR/tmpfs/stderr.tmp" >&2 echo "$PROGNAME: tar-lvm-one failed on host '$vmhost_v'" >&2 ec=1 fi else echo -n \ | awk 'BEGIN {printf("%s\n", ENVIRON["'"$devpassvar"'"]);}' \ | ssh -x -A "root@$vmhost_h" "bash -c 'read -s TAR_LVM_ONE_PASSWORD; export TAR_LVM_ONE_PASSWORD; bgrun $MAILFROM $SMTPSERVER ${MAILTO[@]} -- tar-lvm-one $TAR_LVM_ONE_F $ARG_MODE $ARG_LEVEL'" 2>"$WORKDIR/tmpfs/stderr.tmp" if [[ $? -eq 0 ]]; then if [[ -z "$parvmhosts" ]]; then parvmhosts="$vmhost/0" else parvmhosts="$parvmhosts $vmhost/0" fi else grep -Ev '^stdin: is not a tty$' "$WORKDIR/tmpfs/stderr.tmp" >&2 echo "$PROGNAME: tar-lvm-one failed on host '$vmhost_v'" >&2 ec=1 fi fi fi # wait until not all tar-lvm-one processes run on vms/hosts while true; do sleep "$hostperiod" stmsg="VMs|hosts> $parvmhosts (max: $parvmhostnum)" [[ "$ARG_V" = y ]] && echo "$stmsg" echo "$PROGNAME: $stmsg" > /dev/console for vh_fail in $parvmhosts; do vh="$(echo "$vh_fail" | cut -d/ -f1)" vh_v="$(echo "$vh" | sed -r 's#^([^:]*):.*$#\1#')" vh_h="$(echo "$vh" | sed -r 's#^[^:]*:(.*)$#\1#')" fail="$(echo "$vh_fail" | cut -d/ -f2)" fail_now=1 if [[ -e "$WORKDIR/tmpfs/tar-lvm-one.runs" ]]; then rm -fr "$WORKDIR/tmpfs/tar-lvm-one.runs" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete temporary file 'tar-lvm-one.runs'" >&2 echo "$PNSPACES from the directory '$WORKDIR/tmpfs' on the tmpfs filesystem" >&2 echo "$PNSPACES and thus cannot find out if tar-lvm-one still runs" >&2 echo "$PNSPACES on host '$vh_v'" >&2 fail_now=0 fi fi if [[ "$fail_now" != 0 ]]; then ssh -x -A "root@$vh_h" "bash -c 'echo -n TAR-LVM-ONE-COMMAND-RUNS: && ps -e -o comm= | grep ^tar-lvm-one\$ || true'" >"$WORKDIR/tmpfs/tar-lvm-one.runs" 2>"$WORKDIR/tmpfs/stderr.tmp" if [[ $? -ne 0 ]]; then n grep -Ev '^stdin: is not a tty$' "$WORKDIR/tmpfs/stderr.tmp" >&2 echo "$PROGNAME: cannot find out if tar-lvm-one still runs" >&2 echo "$PNSPACES on host '$vh_v'" >&2 fail_now=0 fi fi if [[ "$fail_now" != 0 ]]; then if [[ "$(grep '^TAR-LVM-ONE-COMMAND-RUNS:tar-lvm-one$' "$WORKDIR/tmpfs/tar-lvm-one.runs" | wc -l)" = 0 ]] then parvmhosts="$(echo "$parvmhosts" | sed -r "s#^(.*[[:blank:]])?$vh/[[:digit:]]+([[:blank:]].*)?\$#\\1\\2#g")" else parvmhosts="$(echo "$parvmhosts" | sed -r "s#^(.*[[:blank:]])?($vh)/[[:digit:]]+([[:blank:]].*)?\$#\\1 \\2/0 \\3#g")" fi else fail="$((fail + 1))" if [[ "$fail" -ge "$SSHCHECK_RETRIES" ]]; then parvmhostnum="$((parvmhostnum - 1))" parvmhosts="$(echo "$parvmhosts" | sed -r "s#^(.*[[:blank:]])?$vh/[[:digit:]]+([[:blank:]].*)?\$#\\1\\2#g")" echo "$PROGNAME: '$fail' consecutive SSH failures for host '$vh_v'" >&2 echo "$PNSPACES occured, maximal number of hosts to backup in parallel" >&2 echo "$PNSPACES decreased to '$parvmhostnum'" >&2 else parvmhosts="$(echo "$parvmhosts" | sed -r "s#^(.*[[:blank:]])?$vh/[[:digit:]]+([[:blank:]].*)?\$#\\1$vh/$fail\\2#g")" echo "$PROGNAME: '$fail' consecutive SSH failures for host '$vh_v'" >&2 echo "$PNSPACES occured, waiting for next attempt ..." >&2 fi fi parvmhosts="$(echo "$parvmhosts" | sed -r 's#[[:blank:]]+# #g')" parvmhosts="$(echo "$parvmhosts" | sed -r 's#^[[:blank:]]*##g')" parvmhosts="$(echo "$parvmhosts" | sed -r 's#[[:blank:]]*$##g')" done [[ "$parvmhostnum" = 0 ]] && break if [[ "$vmhost" != '/' ]]; then [[ "$(echo "$parvmhosts" | sed -r 's#[[:blank:]]+#\n#g' | grep -v '^[[:blank:]]*$' | wc -l)" -lt "$parvmhostnum" ]] && break else echo "$parvmhosts" | grep -Eq '^[[:blank:]]*$' && break fi done # detach the pmdev device from vm (not from pm in the first # iteration) if [[ "$ARG_MODE" != 'clean' || "$parvmhostnum" = 0 ]]; then if [[ -n "$isvm" && -z "$ispm" ]]; then virsh detach-device "$vmhost_v" "$WORKDIR/tmpfs/$DEVDEFFILE" >/dev/null if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot detach backup device from virtual machine '$vmhost_v'," >&2 echo "$PNSPACES cannot continue" >&2 clean_exit 3 fi fi fi # print status info in the clean and check mode if [[ "$vmhost_v" != '/' ]]; then if [[ "$ARG_MODE" = 'clean' ]]; then echo "$PROGNAME: cleanup on host '$vmhost_v' finished" echo elif [[ "$ARG_MODE" = 'check' ]]; then echo "$PROGNAME: check on host '$vmhost_v' finished" echo fi fi # exit if number of hosts to backup in parallel reached 0 if [[ "$parvmhostnum" = 0 ]]; then echo "$PROGNAME: maximal number of hosts to backup in parallel reached 0," >&2 echo "$PNSPACES cannot continue" >&2 clean_exit 4 fi # wait some time so that all vms are ready to accept ssh connections even after fast pm backup if [[ "$ARG_MODE" != 'clean' ]]; then [[ -n "$ispm" && "$PMBACKUP" = 'true' ]] && sleep "$POSTPM_DELAY" fi # set flag to recognize first iteration (pm backup) unset ispm done # print result in check mode or perform local cleanup if requested if [[ "$ARG_MODE" = 'check' ]]; then [[ "$ec" != 0 ]] && result='FAILURE' echo "$PROGNAME: result => $result" echo elif [[ "$ARG_MODE" = 'clean' ]]; then clean [[ $? -ne 0 ]] && ec=1 fi return "$ec" } ###### MAIN ################################################################### ### check tools, parse and check arguments # check if all the external tools are available on the system check_tools grep id ps cat rm mount umount sed true find head ssh awk # parse and check arguments parse_args "$@" check_args [[ "$ARG_MODE" = 'help' ]] && usage # check if the script is called by root check_root_id # check if the script is not running yet check_if_running # check working directory check_work_dir # read the configuration file and check configuration read_config_file 'all' "$CONFDIR/$CONFFILE" check_config 'all' # check add-on tools presence check_addon_tools # mount the tmpfs filesystem which is always read/write mount_tmpfs "$TMPFS_SIZE" # create file which maps uuids to device names create_uuid_file # create libvirt device definition file to access the backup device # from virtual machines if at least one should be backed up [[ "${#VM[@]}" -gt 0 ]] && create_devdef_file "$PMDEV" # prompt for password to decrypt the backup device if needed unset BKPDEVPASSWORD if [[ ( "$ARG_MODE" = 'pre' || "$ARG_MODE" = 'run' || "$ARG_MODE" = 'post' || "$ARG_MODE" = 'all' || "$ARG_MODE" = 'check' ) && "$PASSPROMPT" = 'true' ]]; then echo -n 'Enter the password for the backup device: ' read -s BKPDEVPASSWORD echo export BKPDEVPASSWORD fi ### backup all virtual machines, hosts and optionally also the physical ### machine at the beginning or just some part of it depending ### on the mode or perform cleanup instead if requested all "$CONFFILE_ONE_LOCAL" "$CONFFILE_ONE_SHARED" \ BKPDEVPASSWORD "$VMCHECK_PERIOD" [[ $? -ne 0 ]] && EC=3 ### clean exit clean_exit "$EC" tar-lvm/install/sbin/tar-lvm0000755002342000234200000017253414041736657015334 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: tar-lvm # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### # set program name PROGNAME='tar-lvm' PNSPACES=' ' # set versions SCRIPT_VERSION='00.20' SUITE_VERSION='00.80' # set configuration directory and file WORKDIR="/var/local/$PROGNAME" CONFFILE="/usr/local/etc/$PROGNAME/$PROGNAME.conf" # set uuid filename UUID_FILE="uuid.list.$PROGNAME" # set tmpfs size in bytes TMPFS_SIZE="$((20*1024*1024))" # exit commands array and clean exit running flag unset EXIT_CMDS CE_RUNNING=1 # script result as a string, i.e. its exit status printed at the end RESULT='FAILURE' # unset global array variables unset RWFS unset RWLV unset SNLV # set default values that might be redefined in the configuration file # LVSNAPSUFFIX is a suffix that is appended to the lvm logical volume names # in order to get a name of a corresponding snapshot LVSNAPSUFFIX='.tar-lvm' # disables GNU/tar ACLs support if set unset NOACLS # the following array variables are used to store the lvm logical volumes # or non-lvm devices, i.e. their type ("lvm"), name, group, snapshot size # and list of directories to exclude or type ("fs"), name, device path # and list of directories to exclude unset LVFSTYPE unset LVFSNAME unset FSDEV unset LVGROUP unset LVSNAPSIZE unset LVFSEXCLUDE # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN are available on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 2 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 2 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - tar filesystems on devices or lvm volumes into incremental backups" echo echo " ($PROGNAME version: $SCRIPT_VERSION, $PROGNAME suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h | help" echo " $PROGNAME [-v] pre" echo " $PROGNAME [-v] [-f] run level outdir" echo " $PROGNAME [-v] post" echo " $PROGNAME check level outdir" echo " $PROGNAME [-v] clean" echo echo ' -h | help ... print help and exit' echo ' pre ... prepare the system for the backup, i.e. remount filesystems' echo ' read-only, create lvm snapshots and remount filesystems on top' echo ' of lvm back read-write' echo ' run ... run the backup of the read-only filesystems and lvm snapshots' echo ' post ... finalize the backup, i.e. remount the read-only filesystems back' echo ' read-write and remove the lvm snapshots' echo ' check ... check that all backup archives of the specified level and' echo ' of all preceding levels exist' echo ' clean ... perform cleanup, this may be needed if the script terminates' echo ' unexpectedly, e.g. it it is killed by a signal, if the system' echo ' is restarted or halted etc.' echo echo ' level ... level of the incremental backup, i.e. non-negative integer' echo ' outdir ... output directory for the backups and logs' echo ' -v ... verbosely print what is done' echo ' -f ... force the backup, i.e. delete the archives and logs of the same' echo ' and higher level than level if they exist instead of quitting' echo exit "$ec" } # parse_args() arg1 ... argN # parse the command-line arguments and set the global variables ARG_MODE, # ARG_LEVEL, ARG_OUTDIR, ARG_V and ARG_F. # return: exit - wrong arguments are given # 0 - ok parse_args() { local arg ARGNUM=0 ARG_MODE='' ARG_LEVEL='' ARG_OUTDIR='' ARG_V='n' ARG_F='n' for arg in "$@"; do case "$arg" in '-v') [[ "$ARG_V" = 'y' ]] && usage 1 ARG_V='y' ;; '-f') [[ "$ARG_F" = 'y' ]] && usage 1 ARG_F='y' ;; *) if [[ "${arg:0:1}" = '-' \ && ( "$arg" != '-h' || "$ARGNUM" -gt 0 ) ]]; then usage 1 else ARGNUM="$((ARGNUM+1))" case "$ARGNUM" in 1) case "$arg" in '-h' | 'help') ARG_MODE='help' ;; 'pre' | 'run' | 'post' | 'clean' | 'check') ARG_MODE="$arg" ;; *) usage 1 ;; esac ;; 2) ARG_LEVEL="$arg" ;; 3) ARG_OUTDIR=$(echo -n "$arg" | sed 's#/*$##') ;; *) usage 1 ;; esac fi ;; esac done if [[ -z "$ARG_MODE" ]]; then usage 1 elif [[ "$ARG_MODE" = 'help' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_V" = 'y' || "$ARG_F" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'pre' || "$ARG_MODE" = 'post' || "$ARG_MODE" = 'clean' ]]; then [[ "$ARGNUM" -ne 1 || "$ARG_F" = 'y' ]] && usage 1 elif [[ "$ARG_MODE" = 'run' || "$ARG_MODE" = 'check' ]]; then [[ "$ARGNUM" -ne 3 ]] && usage 1 fi return 0 } # check_args() # check that the command-line arguments are correct, must be called after # parse_args # return: exit - the arguments are not correct # 0 - ok check_args() { if [[ "$ARG_MODE" = 'run' || "$ARG_MODE" = 'check' ]]; then if ! echo "$ARG_LEVEL" | grep -Eq '^[0-9]+$'; then echo "$PROGNAME: level must be a non-negative integer" >&2 exit 1 fi if [[ ! -d "$ARG_OUTDIR" ]]; then echo "$PROGNAME: output directory '$ARG_OUTDIR' doesn't exist" >&2 exit 1 fi fi return 0 } # add_exit_cmd exit_cmd # add exit command to be invoked when clean_exit is invoked, the command # is just one argument to be invoked via eval # return: 0 - ok add_exit_cmd() { local cmd="$1" local i i="${#EXIT_CMDS[@]}" EXIT_CMDS[i]="$cmd" return 0; } # del_exit_cmd [exit_cmd_number] # remove exit command to be invoked when clean_exit is invoked, the command # number can be specified, if it is not, the last command is removed if any # return: 0 - ok del_exit_cmd() { local i="$1" local j if [[ "${#EXIT_CMDS[@]}" -gt 0 ]]; then if [[ -z "$i" ]]; then j="$(( ${#EXIT_CMDS[@]} - 1 ))" unset EXIT_CMDS[j] else EXIT_CMDS[i]='' fi fi return 0; } # run_exit_cmds # invoke all registered exit commands in the opposite order in which they # were registered, but do not actually exit, then remove all exit commands # return: 0 - ok # 1 - some exit command failed run_exit_cmds() { local i n local cmd local eval_ec ec=0 n="${#EXIT_CMDS[@]}" for ((i=n-1; i>=0; i--)); do cmd="${EXIT_CMDS[i]}" if [[ -n "$cmd" ]]; then eval "$cmd" eval_ec="$?" fi if [[ "$eval_ec" -ne 0 ]]; then ec=1 fi done unset EXIT_CMDS return "$ec" } # clean_exit [ec] # exit cleanly, i.e. invoke all registered exit commands before exiting # in the opposite order in which they were registered, the exit commands # can be (de)registered by using add_exit_cmd/del_exit_cmd, finally, # exit with the given exit code or with 1 if no exit code is given # and some command fails # return: exit clean_exit() { local ec_orig="${1:-0}" local i n local cmd local ec=0 CE_RUNNING=0 run_exit_cmds [[ "$?" -ne 0 ]] && ec=1 [[ "$ec_orig" = 0 && "$ec" != 0 ]] && ec_orig="$ec" exit "$ec_orig" } # print_result() # print the final status of the script, i.e. its result # return: 0 - ok print_result() { if [[ "$ARG_V" = 'y' ]]; then echo echo "$PROGNAME: result => $RESULT" echo fi } # check_root_id() # check that the command is called by root # return: exit - the command is not called by root # 0 - ok or clean exit in progress check_root_id() { [[ "$CE_RUNNING" = 0 ]] && return 0 if [[ $(id -u) != 0 ]]; then echo "$PROGNAME: you must be root in order to use this script" >&2 clean_exit 2 fi return 0 } # check_if_running() # check that this script is not running yet # return: exit - the script is already running # 0 - ok check_if_running() { if [[ -f "/var/run/$PROGNAME.pid" ]]; then if [[ "$(ps -p "$(cat "/var/run/$PROGNAME.pid")" -o 'comm=')" = "$PROGNAME" ]]; then echo "$PROGNAME: $PROGNAME already running" >&2 clean_exit 2 else rm -f "/var/run/$PROGNAME.pid" fi fi echo "$$" >"/var/run/$PROGNAME.pid" [[ "$?" -eq 0 ]] && add_exit_cmd "rm -f '/var/run/$PROGNAME.pid'" return 0 } # check_work_dir mode # check that the working directory exists and contains the subdirectories # mnt and tmp with proper access permissions depending on the script mode # (pre, run or post) # return: exit - the working directory isn't all right # 0 - ok or clean exit in progress check_work_dir() { [[ "$CE_RUNNING" = 0 ]] && return 0 local ok=0 local mode="$1" if [[ ! -d "$WORKDIR" || ! -r "$WORKDIR" ]]; then echo "$PROGNAME: the directory '$WORKDIR' doesn't exist or" >&2 echo "$PNSPACES isn't readable" >&2 ok=1 fi if [[ ! -d "$WORKDIR/mnt" || ! -r "$WORKDIR/mnt" ]]; then echo "$PROGNAME: the directory '$WORKDIR/mnt' doesn't exist or" >&2 echo "$PNSPACES isn't readable" >&2 ok=1 fi if [[ "$mode" = 'pre' ]]; then if [[ ! -d "$WORKDIR/tmp" || ! -r "$WORKDIR/tmp" \ || ! -w "$WORKDIR/tmp" ]]; then echo "$PROGNAME: the directory '$WORKDIR/tmp' doesn't exist," >&2 echo "$PNSPACES isn't readable or writable" >&2 ok=1 fi else if [[ ! -d "$WORKDIR/tmp" || ! -r "$WORKDIR/tmp" ]]; then echo "$PROGNAME: the directory '$WORKDIR/tmp' doesn't exist or" >&2 echo "$PNSPACES isn't readable" >&2 ok=1 fi fi [[ "$ok" != 0 ]] && clean_exit 2 return 0 } # check_outdir() # check that the output directory contains readable and writable cache # and log subdirectories and if not then create them # return: exit - the cache and log subdirectories do not exist and cannot be # created, they're not readable and writable # 0 - ok or clean exit in progress check_outdir() { local dir for dir in "$ARG_OUTDIR/cache" "$ARG_OUTDIR/log"; do if [[ ! -d "$dir" ]]; then [[ -n "$ARG_V" ]] \ && echo "$PROGNAME: directory '$dir' doesn't exist, creating it..." mkdir "$dir" if [[ "$?" -ne 0 ]]; then echo "$PROGNAME: cannot create directory '$dir'" >&2 clean_exit 2 fi fi [[ ! -r "$dir" ]] && chmod u+r "$dir" [[ ! -w "$dir" ]] && chmod u+w "$dir" if [[ ! -w "$dir" || ! -r "$dir" ]]; then echo "$PROGNAME: directory '$dir' not readable and writable" >&2 clean_exit 2 fi done return 0 } # mount_tmpfs() size # mount the tmpfs filesystem to the directory mntdir if not mounted yet, # this filesystem remains read/write even if the tmp directory is already # on a read-only filesystem # return: exit - cannot mount the tmpfs filesystem # 0 - ok or clean exit in progress mount_tmpfs() { [[ "$CE_RUNNING" = 0 ]] && return 0 local size="$1" if ! grep -Eq "^tmpfs[[:blank:]]+$WORKDIR/tmpfs" /proc/mounts; then mount -t tmpfs -o size="$size",mode=755,uid=0,gid=0 \ tmpfs "$WORKDIR/tmpfs" if [[ $? -eq 0 ]]; then add_exit_cmd 'umount_tmpfs' else echo "$PROGNAME: cannot mount the tmpfs filesystem to the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs'" >&2 clean_exit 2 fi fi return 0 } # umount_tmpfs() # unmount the tmpfs filesystem if it is mounted # return: 0 - ok (or not mounted) # 1 - cannot unmount the tmpfs filesystem umount_tmpfs() { if grep -Eq "^tmpfs[[:blank:]]+$WORKDIR/tmpfs" /proc/mounts; then umount "$WORKDIR/tmpfs" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot unmount the tmpfs filesystem from the directory" >&2 echo "$PNSPACES '$WORKDIR/tmpfs'" >&2 return 1 fi fi return 0 } # read_config_file cfg_file # read the configuration file cfg_file, each line of the configuration file # which is not a comment consists of whitespace separated words, the words # can be optionally enclosed in double quotes and can therefore also contain # whitespace characters - space, tab, double quote and backslash may be part # of a word too if they're escaped by a backslash, lines starting with # # (optional whitespaces may precede #) are comments, currently only # the lvsnapsuffix, noacls, fs and lv lines are supported, they are used # to define the NOACLS, LVFS..., LV... and FS... global variables # return: exit - the config file isn't readable or doesn't have correct format # 0 - ok or clean exit in progress read_config_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local cfg_file="$1" cfg_cont local linenum line str fch wrd local ifs local islss=1 isnoacls=1 n q i local excl # check that the config file exists and is readable if [[ -f "$cfg_file" && -r "$cfg_file" ]]; then cfg_cont="$(cat "$cfg_file")" else echo "$PROGNAME: cannot read the configuration file '$cfg_file'" >&2 clean_exit 2 fi # remove space, tab and newline from IFS so that the whole line # including the leading whitespace is read by the read built-in ifs="$IFS" IFS="" # read and process all lines from the file cfg_file linenum=0 while read -r line; do linenum=$((linenum+1)) unset args local -a args # remove comments, i.e. lines beginning with #, optional whitespace # may precede the # character str=$(echo "$line" | sed -r 's/^([[:blank:]]*)#.*$/\1/') # read words from the line read and store them into the args array while true; do # remove leading whitespace str=$(echo "$str" | sed -nr 's#^[[:blank:]]*(.*)?$#\1#p') # if there is no first character, i.e. no word, then break fch="${str:0:1}" if [[ -z "$fch" ]]; then break # if the first character isn't ", the word is delimited # by whitespace that is not escaped by \ elif [[ "$fch" != '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^(([^[:blank:]"\\]*[\\][[:blank:]"\\])*[^[:blank:]"\\]*)([[:blank:]]+(.*))?$#\4#p') # if the first character is ", the word is delimited by # double quotes that are not escaped by \ and the quoted words # by whitespace elif [[ "$fch" = '"' ]]; then wrd=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\1#p') str=$(echo "$str" | sed -nr 's#^"(([^"\\]*[\\][[:blank:]"\\])*[^"\\]*)"([[:blank:]]+(.*))?$#\4#p') fi # replace all characters escaped by \ in the word read wrd=$(echo "$wrd" | sed -r 's#[\\]([[:blank:]"\\])#\1#g') # store the resulting word into the args array args[${#args[*]}]="$wrd" done # if the line has at least one word, check if it is correct and # assign values to appropriate variables if [[ ${#args[*]} -gt 0 ]]; then case "${args[0]}" in 'lvsnapsuffix') if [[ "$islss" = 0 ]]; then echo "$PROGNAME: lvsnapsuffix defined multiple times in the config file" >&2 clean_exit 2 fi islss=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi LVSNAPSUFFIX="${args[1]}" ;; 'noacls') if [[ "$isnoacls" = 0 ]]; then echo "$PROGNAME: noacls defined multiple times in the config file" >&2 clean_exit 2 fi isnoacls=0 if [[ ${#args[*]} -ne 2 || -z "${args[1]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi NOACLS="${args[1]}" ;; 'fs') if [[ ${#args[*]} -lt 3 \ || -z "${args[1]}" || -z "${args[2]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#LVFSTYPE[*]}" LVFSTYPE[n]="fs" LVFSNAME[n]="${args[1]}" FSDEV[n]="${args[2]}" q=3 ;; 'lv') if [[ ${#args[*]} -lt 4 || -z "${args[1]}" \ || -z "${args[2]}" || -z "${args[3]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi n="${#LVFSTYPE[*]}" LVFSTYPE[n]='lv' LVFSNAME[n]="${args[1]}" LVGROUP[n]="${args[2]}" LVSNAPSIZE[n]="${args[3]}" q=4 ;; *) echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 ;; esac if [[ "${args[0]}" = 'fs' || "${args[0]}" = 'lv' ]]; then for ((i=q; i<${#args[*]}; i++)); do if [[ -z "${args[i]}" ]]; then echo "$PROGNAME: incorrect format of the config line number $linenum:" >&2 echo "$line" >&2 clean_exit 2 fi # replace an inner ' by '"'"' so that eval can be used # later because LVFSEXCLUDE[n] is a whitespace separated # list of directories enclosed in single quotes (bash # doesn't provide two dimensional arrays) excl=$(echo "${args[i]}" \ | sed "s#'#'\"'\"'#g") LVFSEXCLUDE[n]="${LVFSEXCLUDE[n]} '$excl'" done fi fi done <&2 exit 1 # return from the subshell created because of the pipe fi prevnm="$nm" fst=1 done [[ "$?" -ne 0 ]] && clean_exit 2 # really exit # check LVSNAPSIZEs for ((i=0; i<${#LVFSTYPE[i]}; i++)); do if [[ "${LVFSTYPE[i]}" = 'lv' ]]; then name="${LVFSNAME[i]}" size="${LVSNAPSIZE[i]}" if ! echo "$size" | grep -Eq '^[1-9][0-9]*%$'; then echo "$PROGNAME: snapshot size of the '$name' lvm volume must be a positive integer" >&2 echo "$PNSPACES with a % suffix, because it specifies the size with respect" >&2 echo "$PNSPACES to the origin, edit the config file" >&2 exit 1 # return from the subshell created because of the pipe fi [[ "$?" -ne 0 ]] && clean_exit 2 # really exit fi done return 0 } # check_uuid_presence() # check whether the UUID syntax is used in the configuration # return: # 0 - used # 1 - not used check_uuid_presence() { local n="${#LVFSTYPE[*]}" local i for ((i=0; i/dev/null; then echo "$PROGNAME: 'blkid' binary not found and UUID= syntax used" >&2 echo "$PNSPACES in the config file" >&2 ec=2 fi fi # check that no filesystems on top of lvm are present lvflag=1 for lvfstype in "${LVFSTYPE[@]}"; do [[ "$lvfstype" = 'lv' ]] && lvflag=0 && break done if [[ "$lvflag" = 0 ]]; then if ! which lvcreate >/dev/null || ! which lvremove >/dev/null \ || ! which lvdisplay >/dev/null; then echo "$PROGNAME: 'lvcreate', 'lvremove' or 'lvdisplay' binary not found" >&2 echo "$PNSPACES and filesystems on top of lvm specified in the config file" >&2 ec=2 fi fi [[ "$ec" -ne 0 ]] && clean_exit "$ec" return 0 } # get_timestamp # print timestamp in the RFC-3339 format # return: # 0 - ok get_timestamp() { if [[ "$ARG_V" = 'y' ]]; then echo -n "$PROGNAME: " date --rfc-3339=seconds fi return 0 } # check_proc_mounts() ('real'|'copy') # check that the file /proc/mounts or its copy in the tmp workdir is # available and that it is readable # return: exit - the file isn't available in the pre, run or post mode # 0 - ok or clean exit in progress # 1 - the file isn't available in the clean mode check_proc_mounts() { [[ "$CE_RUNNING" = 0 ]] && return 0 local file="$1" if [[ "$file" = 'real' ]]; then if [[ ! -f '/proc/mounts' || ! -r '/proc/mounts' ]]; then echo "$PROGNAME: the file '/proc/mounts' must exist and be readable, is the /proc" >&2 echo "$PNSPACES filesystem mounted?" >&2 clean_exit 2 fi elif [[ "$file" = 'copy' ]]; then if [[ "$ARG_MODE" = 'pre' ]]; then if [[ -f "$WORKDIR/tmp/mounts" ]]; then echo "$PROGNAME: the file '$WORKDIR/tmp/mounts' already exists which" >&2 echo "$PNSPACES indicates that this command in the pre mode has already" >&2 echo "$PNSPACES successfully finished its preparation, use this command" >&2 echo "$PNSPACES in the post mode to revert to the original state and" >&2 echo "$PNSPACES rather check if the filesystems were remounted properly" >&2 echo "$PNSPACES and the lvm snaphots removed (this might not be the case" >&2 echo "$PNSPACES if you ran this command in the pre mode some time ago" >&2 echo "$PNSPACES with different configuration)" >&2 clean_exit 2 fi elif [[ "$ARG_MODE" = 'post' ]]; then if [[ ! -f "$WORKDIR/tmp/mounts" || ! -r "$WORKDIR/tmp/mounts" ]]; then echo "$PROGNAME: the file '$WORKDIR/tmp/mounts' isn't available or readable," >&2 echo "$PNSPACES this indicates that this command in the pre mode hasn't finished" >&2 echo "$PNSPACES successfully prior to the post mode and you therefore cannot" >&2 echo "$PNSPACES use the post mode now, you must check and adjust the mounted" >&2 echo "$PNSPACES filesystems and lvm snapshots manually by using the mount, umount," >&2 echo "$PNSPACES lvs, lvremove or another lvm commands if you want to do so" >&2 clean_exit 2 fi elif [[ "$ARG_MODE" = 'run' ]]; then if [[ ! -f "$WORKDIR/tmp/mounts" || ! -r "$WORKDIR/tmp/mounts" ]]; then echo "$PROGNAME: the file '$WORKDIR/tmp/mounts' isn't available or readable," >&2 echo "$PNSPACES this indicates that this command in the pre mode hasn't finished" >&2 echo "$PNSPACES successfully prior to the run mode and you therefore cannot" >&2 echo "$PNSPACES use the run mode now" >&2 clean_exit 2 fi elif [[ "$ARG_MODE" = 'clean' ]]; then if [[ ! -f "$WORKDIR/tmp/mounts" || ! -r "$WORKDIR/tmp/mounts" ]]; then echo "$PROGNAME: the file '$WORKDIR/tmp/mounts' isn't available or readable," >&2 echo "$PNSPACES this indicates that this command in the pre mode hasn't finished" >&2 echo "$PNSPACES successfully prior to the clean mode, filesystems cannot be" >&2 echo "$PNSPACES remounted back" >&2 return 1 fi fi fi return 0 } # store_proc_mounts() # store the file /proc/mounts into the tmp workdir # return: exit - the file /proc/mounts isn't readable or cannot be copied # 0 - ok or clean exit in progress store_proc_mounts() { [[ "$CE_RUNNING" = 0 ]] && return 0 check_proc_mounts 'real' check_proc_mounts 'copy' cp -f /proc/mounts "$WORKDIR/tmp" if [[ "$?" -ne 0 ]]; then echo "$PROGNAME: cannot copy the file '/proc/mounts' into '$WORKDIR/tmp'" >&2 clean_exit 2 fi return 0 } # remove_proc_mounts() # remove the copy of the file /proc/mounts from the tmp workdir # return: 0 - ok # 1 - cannot remove the file remove_proc_mounts() { rm -f "$WORKDIR/tmp/mounts" if [[ "$?" -ne 0 ]]; then echo "$PROGNAME: cannot remove the file '$WORKDIR/tmp/mounts'," >&2 echo "$PNSPACES check and remove it manually!!! this is important" >&2 echo "$PNSPACES because the existence of this file indicates the state" >&2 echo "$PNSPACES of the $PROGNAME cycle, i.e. whether the pre, post or run" >&2 echo "$PNSPACES $PROGNAME actions can be executed and how the filesystems" >&2 echo "$PNSPACES were originally mounted" >&2 return 1 fi return 0 } # create_uuid_file # find all block devices in /dev which have uuid assigned and create # uuid file in the tmp directory which maps uuids to devices # return: exit - cannot create the uuid file # 0 - ok or clean exit in progress create_uuid_file() { [[ "$CE_RUNNING" = 0 ]] && return 0 local dev local uuid if [[ -e "$WORKDIR/tmpfs/$UUID_FILE" ]]; then rm -fr "$WORKDIR/tmpfs/$UUID_FILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot delete old file '$WORKDIR/tmpfs/$UUID_FILE'" >&2 echo "$PNSPACES and cannot therefore create updated version" >&2 clean_exit 2 fi fi if check_uuid_presence; then find /dev -type b \ | while read dev; do uuid="$(blkid "$dev" | sed -nr 's#^.*[[:blank:]](PT)?UUID=\"([^\"]*)\".*$#\2#p')" if [[ -n "$uuid" ]]; then echo "$uuid $dev" >>"$WORKDIR/tmpfs/$UUID_FILE" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create file '$WORKDIR/tmpfs/$UUID_FILE'" >&2 clean_exit 2 fi fi done fi return 0 } # get_uuid_device uuid # print path to the device with the given uuid as specified in the uuid file, # print nothing if no device with uuid found # return: 0 - ok get_uuid_device() { local uuid="$1" grep -i "^$uuid[[:blank:]]" "$WORKDIR/tmpfs/$UUID_FILE" \ | head -1 \ | sed -nr 's#^[^[:blank:]]+[[:blank:]]+([^[:blank:]]+)$#\1#p' return 0 } # get_fs_type() device # print device filesystem type from /proc/mounts or nothing it device # wasn't found # return: 0 - ok # 1 - device not found in /proc/mounts get_fs_type() { local dev="$1" local mntfile mntdev mnttype mntfile='/proc/mounts' [[ -L "$dev" ]] && dev=$(readlink -mn "$dev") eval 'for line in '$(grep '^/' "$mntfile" | sed -r 's#^(.*)$#"\1"#')'; do mntdev=$(echo "$line" | awk "{print \$1}") mnttype=$(echo "$line" | awk "{print \$3}") [[ -L "$mntdev" ]] && mntdev=`readlink -mn "$mntdev"` if [[ "$mntdev" = "$dev" ]]; then echo "$mnttype" return 0 fi done' return 1 } # is_mount_rw() ("real"|"copy") device # check if device is/was mounted rw, ro or not at all according to the # real /proc/mounts file or its copy stored in the tmp workdir # return: 0 - device is mounted rw # 1 - device is mounted ro # 2 - device isn't mounted is_mount_rw() { local mntfiletype="$1" dev="$2" local mntfile mntdev mntopts mntfile='/proc/mounts' [[ "$mntfiletype" = 'copy' ]] && mntfile="$WORKDIR/tmp/mounts" [[ -L "$dev" ]] && dev=$(readlink -mn "$dev") eval 'for line in '$(grep '^/' "$mntfile" | sed -r 's#^(.*)$#"\1"#')'; do mntdev=$(echo "$line" | awk "{print \$1}") mntopts=$(echo "$line" | awk "{print \$4}") [[ -L "$mntdev" ]] && mntdev=`readlink -mn "$mntdev"` if [[ "$mntdev" = "$dev" ]]; then echo "$mntopts" | tr "," "\n" | grep -Eq "^ro\$" if [[ "$?" -eq 0 ]]; then return 1 else return 0 fi fi done' return 2 } # remount_rwfs_ro() # remount the rw non-lvm filesystems ro and store their list into # the RWFS array variable (pre mode) - the filesystems to remount # are taken from the LVFS... and FS... array variables # return: 0 - all rw non-lvm filesystems remounted ro # 1 - some of the rw non-lvm filesystems not remounted ro remount_rwfs_ro() { local i res ec=0 local fstype fsname fsdev fsnum local uuid i=0 fstype="${LVFSTYPE[0]}" fsnum=0 [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: remounting filesystems not on lvm read-only" while [[ -n "$fstype" ]]; do if [[ "$fstype" = 'fs' ]]; then if echo "${FSDEV[i]:0:5}" | grep -qi '^UUID=$'; then uuid="$(echo "${FSDEV[i]}" | sed -r 's#^....=##')" fsdev="$(get_uuid_device "$uuid")" if [[ -z "$fsdev" || ! -b "$fsdev" ]]; then echo "$PROGNAME: block device with uuid '$uuid' not found" >&2 ec=1 break fi else fsdev="${FSDEV[i]}" if [[ -z "$fsdev" || ! -b "$fsdev" ]]; then echo "$PROGNAME: block device '$fsdev' not found" >&2 ec=1 break fi fi fsname="${LVFSNAME[i]}" is_mount_rw 'copy' "$fsdev" res="$?" if [[ "$res" = 0 ]]; then [[ "$ARG_V" = 'y' ]] \ && echo " filesystem '$fsname', i.e. device '$fsdev'" mount -o remount,ro "$fsdev" if [[ "$?" -eq 0 ]]; then RWFS[${#RWFS[*]}]="$fsdev" fsnum="$((fsnum+1))" else echo "$PROGNAME: cannot remount device '$fsdev' read-only" >&2 ec=1 break fi fi fi fstype="${LVFSTYPE[++i]}" done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $fsnum filesystem(s) not on lvm remounted read-only" return "$ec" } # remount_rwfs_rw() # remount the non-lvm filesystems that were originally mounted rw, but # were remounted ro, back rw (pre mode) - the list of such filesystems # is taken from the RWFS array variable # return: 0 - all former rw non-lvm filesystems were remounted back rw # 1 - some of the former rw non-lvm filesystems not remounted back rw remount_rwfs_rw() { local i ec=0 local fsnum [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: remounting filesystems not on lvm back read-write" fsnum=0 for ((i=0; i<${#RWFS[*]}; i++)); do [[ "$ARG_V" = 'y' ]] \ && echo " device '${RWFS[i]}'" mount -o remount,rw "${RWFS[i]}" if [[ "$?" -eq 0 ]]; then fsnum="$((fsnum+1))" else echo "$PROGNAME: cannot remount '${RWFS[i]}' device back read-write," >&2 echo "$PNSPACES check and remount manually" >&2 ec=1 fi done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $fsnum filesystem(s) not on lvm remounted back read-write" return "$ec" } # remount_rwlv_ro() # remount the rw lvm filesystems ro and store their list into the RWLV # array variable (pre mode) - the filesystems to remount are taken from # the LVFS... and LV... array variables # return: 0 - all rw lvm filesystems remounted ro # 1 - some of the rw lvm filesystems not remounted ro remount_rwlv_ro() { local i res ec=0 local lvtype lvname lvgroup lvdev lvnum i=0 lvtype="${LVFSTYPE[0]}" lvnum=0 [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: remounting filesystems on top of lvm read-only" while [[ -n "$lvtype" ]]; do if [[ "$lvtype" = 'lv' ]]; then lvname="${LVFSNAME[i]}" lvgroup="${LVGROUP[i]}" lvdev="/dev/$lvgroup/$lvname" is_mount_rw 'copy' "$lvdev" res="$?" if [[ "$res" = 0 ]]; then [[ "$ARG_V" = 'y' ]] \ && echo " logical volume '$lvname' in group '$lvgroup', i.e. device '$lvdev'" mount -o remount,ro "$lvdev" if [[ "$?" -eq 0 ]]; then RWLV[${#RWLV[*]}]="$lvdev" lvnum="$((lvnum+1))" else echo "$PROGNAME: cannot remount device '$lvdev' read-only" >&2 ec=1 break fi fi fi lvtype="${LVFSTYPE[++i]}" done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $lvnum filesystem(s) on top of lvm remounted read-only" return "$ec" } # remount_rwlv_rw() # remount the lvm filesystems that were originally mounted rw, but # were remounted ro, back rw (pre mode) - the list of such filesystems # is taken from the RWLV array variable # return: 0 - all former rw lvm filesystems were remounted back rw # 1 - some of the former rw lvm filesystems not remounted back rw remount_rwlv_rw() { local i ec=0 local lvnum [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: remounting filesystems on top of lvm back read-write" lvnum=0 for ((i=0; i<${#RWLV[*]}; i++)); do [[ "$ARG_V" = 'y' ]] \ && echo " device '${RWLV[i]}'" mount -o remount,rw "${RWLV[i]}" if [[ "$?" -eq 0 ]]; then lvnum="$((lvnum+1))" else echo "$PROGNAME: cannot remount '${RWLV[i]}' device back read-write," >&2 echo "$PNSPACES check and remount manually" >&2 ec=1 fi done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $lvnum filesystem(s) on top of lvm remounted back read-write" return "$ec" } # create_lvsnap() # create snapshots of lvm logical volumes and store their list into # the SNLV array variable (pre mode) - the list of the lvm logical # volumes is taken from the LVFS... and LV... array variables # return: 0 - snapshots created # 1 - creation of snapshots failed create_lvsnap() { local i res ec=0 local lvtype lvname lvgroup lvsnapsize lvsnapname snnum i=0 lvtype="${LVFSTYPE[0]}" [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: creating lvm snapshot logical volumes" snnum=0 while [[ -n "$lvtype" ]]; do if [[ "$lvtype" = 'lv' ]]; then lvname="${LVFSNAME[i]}" lvgroup="${LVGROUP[i]}" lvsnapsize="${LVSNAPSIZE[i]}" lvsnapname="$lvname$LVSNAPSUFFIX" [[ "$ARG_V" = 'y' ]] \ && echo " snapshot '$lvsnapname' of origin '$lvname' in group '$lvgroup'" lvcreate -l "${lvsnapsize}ORIGIN" -n "$lvsnapname" \ -s "$lvgroup/$lvname" >/dev/null if [[ "$?" -eq 0 ]]; then SNLV[${#SNLV[*]}]="$lvgroup/$lvsnapname" snnum="$((snnum+1))" else echo "$PROGNAME: cannot create lvm snapshot '$lvsnapname' of logical volume" >&2 echo "$PNSPACES '$lvgroup/$lvname', if the cause is that such a snapshot volume" >&2 echo "$PNSPACES already exists, consider removing it or change the value" >&2 echo "$PNSPACES of lvsnapsuffix in the configuration file, another common" >&2 echo "$PNSPACES cause is that the lvm metadata backup directory or" >&2 echo "$PNSPACES the lvm directory with file locks are already on a read-only" >&2 echo "$PNSPACES filesystem" >&2 ec=1 break fi fi lvtype="${LVFSTYPE[++i]}" done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $snnum lvm snapshot logical volume(s) created" return "$ec" } # remove_lvsnap() # remove snapshots of lvm logical volumes created previously (pre mode) - # the list of such snapshots is taken from the SNLV array variable # return: 0 - previously created snapshots removed # 1 - some of the previously created snapshots not removed remove_lvsnap() { local i ec=0 local snnum [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: removing lvm snapshot logical volumes" snnum=0 for ((i=0; i<${#SNLV[*]}; i++)); do [[ "$ARG_V" = 'y' ]] \ && echo " snapshot '${SNLV[i]}'" lvremove -f "${SNLV[i]}" >/dev/null if [[ "$?" -eq 0 ]]; then snnum="$((snnum+1))" else echo "$PROGNAME: cannot remove '${SNLV[i]}' snapshot created previously," >&2 echo "$PNSPACES check and remove manually" >&2 ec=1 fi done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $snnum lvm snapshot logical volume(s) removed" return "$ec" } # remount_rwfs_rw_post() # remount the non-lvm filesystems that were rw during the pre mode and are # not unmounted back rw (post mode) - each non-lvm filesystem stored in # the LVFS... and FS... array variables is examined (each of the type "fs"), # its current and previous states are determined from the /proc/mounts file # and its copy stored into the tmp workdir during the pre phase # return: 0 - all former rw non-lvm filesystems were remounted back rw # 1 - some of the former rw non-lvm filesystems that are not unmounted # not remounted back rw remount_rwfs_rw_post() { local ec=0 local i reswas resis local fstype fsname fsdev fsnum local uuid i=0 fstype="${LVFSTYPE[0]}" [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: remounting filesystems not on lvm back read-write" fsnum=0 while [[ -n "$fstype" ]]; do if [[ "$fstype" = 'fs' ]]; then if echo "${FSDEV[i]:0:5}" | grep -qi '^UUID=$'; then uuid="$(echo "${FSDEV[i]}" | sed -r 's#^....=##')" fsdev="$(get_uuid_device "$uuid")" if [[ -z "$fsdev" || ! -b "$fsdev" ]]; then echo "$PROGNAME: block device with uuid '$uuid' not found" >&2 ec=1 continue fi else fsdev="${FSDEV[i]}" if [[ -z "$fsdev" || ! -b "$fsdev" ]]; then echo "$PROGNAME: block device '$fsdev' not found" >&2 ec=1 continue fi fi fsname="${LVFSNAME[i]}" is_mount_rw 'copy' "$fsdev" reswas="$?" is_mount_rw 'real' "$fsdev" resis="$?" case "$reswas" in 0) if [[ "$resis" = 0 ]]; then echo "$PROGNAME: device '$fsdev' was mounted read-write during the pre phase" echo "$PNSPACES and it is already mounted read-write, skipping ..." elif [[ "$resis" = 1 ]]; then [[ "$ARG_V" = 'y' ]] \ && echo " filesystem '$fsname', i.e. device '$fsdev'" mount -o remount,rw "$fsdev" if [[ "$?" -eq 0 ]]; then fsnum="$((fsnum+1))" else echo "$PROGNAME: cannot remount device '$fsdev' back read-write," >&2 echo "$PNSPACES check and remount manually" >&2 ec=1 fi elif [[ "$resis" = 2 ]]; then echo "$PROGNAME: device '$fsdev' was mounted read-write during the pre phase," echo "$PNSPACES but it is unmounted, skipping ..." fi ;; 1) if [[ "$resis" = 0 ]]; then echo "$PROGNAME: device '$fsdev' was mounted read-only during the pre phase," echo "$PNSPACES but is mounted read-write, skipping ..." elif [[ "$resis" = 2 ]]; then echo "$PROGNAME: device '$fsdev' was mounted read-only during the prs phase," echo "$PNSPACES but it is unmounted, skipping ..." fi ;; 2) if [[ "$resis" = 0 ]]; then echo "$PROGNAME: device '$fsdev' was unmounted during the pre phase and" echo "$PNSPACES it is mounted read-write, skipping ..." elif [[ "$resis" = 1 ]]; then echo "$PROGNAME: device '$fsdev' was unmounted during the pre phase and" echo "$PNSPACES it is mounted read-only, skipping ..." fi ;; esac fi fstype="${LVFSTYPE[++i]}" done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $fsnum filesystem(s) not on lvm remounted back read-write" return "$ec" } # remount_rwlv_rw_clean() # remount the lvm filesystems that were rw during the pre mode and are # not unmounted back rw (clean mode) - each lvm filesystem stored in # the LVFS... and FS... array variables is examined (each of the type "lv"), # its current and previous states are determined from the /proc/mounts file # and its copy stored into the tmp workdir during the pre phase # return: 0 - all former rw lvm filesystems were remounted back rw # 1 - some of the former rw lvm filesystems that are not unmounted # not remounted back rw remount_rwlv_rw_clean() { local ec=0 local i reswas resis local lvtype lvname lvdev lvdev2 lvnum local uuid i=0 lvtype="${LVFSTYPE[0]}" [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: remounting filesystems on top of lvm back read-write" lvnum=0 while [[ -n "$lvtype" ]]; do if [[ "$lvtype" = 'lv' ]]; then lvname="${LVFSNAME[i]}" lvgroup="${LVGROUP[i]}" lvdev="/dev/$lvgroup/$lvname" lvdev2="/dev/mapper/$(echo "$lvgroup" | sed -r 's#-#--#g')-$(echo "$lvname" | sed -r 's#-#--#g')" if [[ -z "$lvdev" || ! -b "$lvdev" ]]; then echo "$PROGNAME: block device '$lvdev' not found" >&2 ec=1 continue fi lvname="${LVFSNAME[i]}" is_mount_rw 'copy' "$lvdev" reswas="$?" if [[ "$reswas" = 2 ]]; then is_mount_rw 'copy' "$lvdev2" reswas="$?" fi is_mount_rw 'real' "$lvdev" resis="$?" if [[ "$resis" = 2 ]]; then is_mount_rw 'copy' "$lvdev2" resis="$?" fi case "$reswas" in 0) if [[ "$resis" = 0 ]]; then echo "$PROGNAME: device '$lvdev' was mounted read-write during the pre phase" echo "$PNSPACES and it is already mounted read-write, skipping ..." elif [[ "$resis" = 1 ]]; then [[ "$ARG_V" = 'y' ]] \ && echo " logical volume '$lvname' in group '$lvgroup', i.e. device '$lvdev'" mount -o remount,rw "$lvdev" if [[ "$?" -eq 0 ]]; then lvnum="$((lvnum+1))" else echo "$PROGNAME: cannot remount device '$lvdev' back read-write," >&2 echo "$PNSPACES check and remount manually" >&2 ec=1 fi elif [[ "$resis" = 2 ]]; then echo "$PROGNAME: device '$lvdev' was mounted read-write during the pre phase," echo "$PNSPACES but it is unmounted, skipping ..." fi ;; 1) if [[ "$resis" = 0 ]]; then echo "$PROGNAME: device '$lvdev' was mounted read-only during the pre phase," echo "$PNSPACES but is mounted read-write, skipping ..." elif [[ "$resis" = 2 ]]; then echo "$PROGNAME: device '$lvdev' was mounted read-only during the prs phase," echo "$PNSPACES but it is unmounted, skipping ..." fi ;; 2) if [[ "$resis" = 0 ]]; then echo "$PROGNAME: device '$lvdev' was unmounted during the pre phase and" echo "$PNSPACES it is mounted read-write, skipping ..." elif [[ "$resis" = 1 ]]; then echo "$PROGNAME: device '$lvdev' was unmounted during the pre phase and" echo "$PNSPACES it is mounted read-only, skipping ..." fi ;; esac fi lvtype="${LVFSTYPE[++i]}" done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $lvnum filesystem(s) on top of lvm remounted back read-write" return "$ec" } # remove_lvsnap_post() # remove snapshots of lvm logical volumes created during the pre phase # (post mode) - all snapshots of the lvm logical volumes specified # in the LVFS... and LV... array variables of type "lv" and have # the LVSNAPSUFFIX suffix are removed # return: 0 - ok, snapshots successfully removed # 1 - some of the snapshots weren't removed remove_lvsnap_post() { local i res ec=0 local lvtype lvname lvgroup lvsnapname snnum i=0 lvtype="${LVFSTYPE[0]}" snnum=0 [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: removing lvm snapshot logical volumes" while [[ -n "$lvtype" ]]; do if [[ "$lvtype" = 'lv' ]]; then lvname="${LVFSNAME[i]}" lvgroup="${LVGROUP[i]}" lvsnapname="$lvname$LVSNAPSUFFIX" lvdisplay "$lvgroup/$lvsnapname" >/dev/null 2>&1 if [[ "$?" -eq 0 ]]; then [[ "$ARG_V" = 'y' ]] \ && echo " snapshot '$lvsnapname' of origin '$lvname' in group '$lvgroup'" lvremove -f "$lvgroup/$lvsnapname" >/dev/null if [[ "$?" -eq 0 ]]; then snnum="$((snnum+1))" else echo "$PROGNAME: cannot remove lvm snapshot '$lvsnapname' of logical volume" >&2 echo "$PNSPACES '$lvgroup/$lvname', check and remove manually" >&2 ec=1 fi else echo "$PROGNAME: cannot find lvm snapshot '$lvgroup/$lvsnapname', skipping ..." >&2 fi fi lvtype="${LVFSTYPE[++i]}" done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $snnum lvm snapshot logical volume(s) removed" return "$ec" } # get_lvfsnameorlst() outvar [logs_flag] # create a list of LVFSNAMEs separated by | and assign it to variable outvar, # optionally prefix each list item with a 'tar.' prefix if logs_flag # is present # return: 0 - ok get_lvfsnameorlst() { local outvar89="$1" local lst89 i89 nm89 local logs_flag="$2" lst89="" i89=0 while [[ -n "${LVFSTYPE[i89]}" ]]; do nm89="${LVFSNAME[i89]}" [[ "${LVFSTYPE[i89]}" = 'lv' ]] && nm89="${LVGROUP[i89]}-$nm89" [[ -n "$logs_flag" ]] && nm89="tar.$nm89" lst89="$lst89$nm89|" i89="$((i89+1))" done [[ -n "$lst89" ]] && lst89="${lst89%|}" eval "$outvar89"'="$lst89"' return 0 } # check_previous_backups() # if backup level is greater than zero then check that all backups of lower # level exist, check the backups of all filesystems specified in the # LVFS..., LV... and FS... array variables # return: exit - all backups of lower level do not exist # 0 - ok or clean exit in progress check_previous_backups() { [[ "$CE_RUNNING" = 0 ]] && return 0 local fstrun=0 local l i type name file local level="$ARG_LEVEL" eqstr eqstr='than' [[ "$ARG_MODE" = 'check' ]] && level="$((level+1))" && eqstr='or equal to' if [[ "$level" -gt 0 ]]; then for ((l=0; "$l" < "$level"; l++)); do i=0 type="${LVFSTYPE[0]}" while [[ -n "$type" ]]; do name="${LVFSNAME[i]}" [[ "$type" = 'lv' ]] && name="${LVGROUP[i]}-$name" for file in "$name.$l.tgz" "$name.$l.snar"; do if [[ ! -f "$ARG_OUTDIR/cache/$file" ]]; then if [[ "$fstrun" = 0 ]]; then echo "$PROGNAME: the following incremental archives with level lower $eqstr $ARG_LEVEL" >&2 echo "$PNSPACES must exist in the directory '$ARG_OUTDIR/cache'" >&2 echo "$PNSPACES otherwise you wouldn't be able to list or restore the backups" >&2 fstrun=1 fi echo " '$file'" >&2 fi done type="${LVFSTYPE[++i]}" done done fi [[ "$fstrun" != 0 ]] && clean_exit 2 return 0 } # check_subsequent_backups() # check that no backups of the same or higher level than the current level # exist, if so either exit (default) or delete those backups (-f switch # given), check the backups of all filesystems specified in the LVFS..., # LV... and FS... array variables # return: exit - some backups of the same or higher level exist (default) # or cannot be deleted (-f switch given) # 0 - ok or clean exit in progress check_subsequent_backups() { [[ "$CE_RUNNING" = 0 ]] && return 0 local f flevel local fstrun=0 delnum local nameorlst [[ "$ARG_V" = 'y' && "$ARG_F" = 'y' ]] \ && echo "$PROGNAME: deleting the following backup archives" get_lvfsnameorlst nameorlst delnum=0 for f in \ $(ls -1 "$ARG_OUTDIR/cache" \ | grep -E '^('"$nameorlst"')[.][0-9]+[.](tgz|snar)$') do flevel=$(echo "$f" | sed -nr 's#^.*[.]([0-9]+)[.][a-z]+$#\1#p') if [[ "$flevel" -ge "$ARG_LEVEL" ]]; then if [[ "$ARG_F" != 'y' ]]; then if [[ "$fstrun" = 0 ]]; then echo "$PROGNAME: the following backups of the same or higher level than $ARG_LEVEL already" >&2 echo "$PNSPACES exist in the directory '$ARG_OUTDIR/cache', either increase" >&2 echo "$PNSPACES the backup level or use the -f switch to delete those archives" fstrun=1 fi echo " '$f'" >&2 else [[ "$ARG_V" = 'y' ]] \ && echo " '$f'" rm -f "$ARG_OUTDIR/cache/$f" if [[ "$?" -eq 0 ]]; then delnum="$((delnum+1))" else echo "$PROGNAME: cannot delete '$ARG_OUTDIR/cache/$f'" >&2 fstrun=1 fi fi fi done [[ "$ARG_V" = 'y' && "$ARG_F" = 'y' ]] \ && echo "$PROGNAME: $delnum backup archive(s) deleted" [[ "$fstrun" != 0 ]] && clean_exit 2 return 0 } # delete_subsequent_logs() # delete the logs of the same or higher level than the current level, # check the logs of all backups of filesystems specified in the # LVFS..., LV... and FS... array variables # return: exit - some logs of the same or higher level than the current level # cannot be deleted # 0 - ok or clean exit in progress delete_subsequent_logs() { [[ "$CE_RUNNING" = 0 ]] && return 0 local f flevel local cntrun=0 delnum local nameorlst [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: deleting the following logs" get_lvfsnameorlst nameorlst logs delnum=0 for f in \ $(ls -1 "$ARG_OUTDIR/log" \ | grep -E '^(df|'"$nameorlst"')[.][0-9]+[.]log$') do flevel=$(echo "$f" | sed -nr 's#^.*[.]([0-9]+)[.][a-z]+$#\1#p') if [[ "$flevel" -ge "$ARG_LEVEL" ]]; then [[ "$ARG_V" = 'y' ]] \ && echo " '$f'" rm -f "$ARG_OUTDIR/log/$f" if [[ "$?" -eq 0 ]]; then delnum="$((delnum+1))" else echo "$PROGNAME: cannot delete '$ARG_OUTDIR/log/$f'" >&2 cntrun=1 fi fi done [[ "$ARG_V" = 'y' ]] \ && echo "$PROGNAME: $delnum log(s) deleted" [[ "$cntrun" != 0 ]] && clean_exit 2 return 0 } # log() verbose msg # log message msg to standard error if verbose equals to 'y' # return: 0 - ok log() { local verbose="$1" msg="$2" local ec=0 [[ "$verbose" = 'y' ]] && echo "$msg" >&2 return "$ec" } # backup() # mount, backup and unmount all the specified filesystems and create logs, # the filesystems are specified in the LVFS..., LV... and FS... array # variables # return: 0 - ok, backup was done # 1 - error occured during the backup, backup aborted backup() { local i ex ret local type name dev uuid exclude local df_fail mounted abort local argv local fstype acl acls local dflog="$ARG_OUTDIR/log/df.$ARG_LEVEL.log" local bklog # set the flag passed to log function, i.e. verbose output # on stderr or log only? if [[ "$ARG_V" = 'y' ]]; then argv='y' else argv='n' fi # get the header line from df and write it to the df log df_fail=1 if ! df -h "$WORKDIR/mnt" \ | grep -E '^Filesystem[[:blank:]]+.*[[:blank:]]+Mounted on$' >"$dflog";\ then echo "$PROGNAME: cannot get filesystem disk space usage information from df," >&2 echo "$PNSPACES the df log '$dflog'" >&2 echo "$PNSPACES will not exist or will be empty" >&2 df_fail=0 fi # backup abort=1 i=0 type="${LVFSTYPE[0]}" while [[ -n "$type" ]]; do mounted=1 if [[ "$type" = 'lv' ]]; then name="${LVGROUP[i]}-${LVFSNAME[i]}" dev="/dev/${LVGROUP[i]}/${LVFSNAME[i]}$LVSNAPSUFFIX" else name="${LVFSNAME[i]}" if echo "${FSDEV[i]:0:5}" | grep -qi '^UUID=$'; then uuid="$(echo "${FSDEV[i]}" | sed -r 's#^....=##')" dev="$(get_uuid_device "$uuid")" if [[ -z "$dev" || ! -b "$dev" ]]; then echo "$PROGNAME: block device with uuid '$uuid' not found" >&2 return 1 fi else dev="${FSDEV[i]}" if [[ -z "$dev" || ! -b "$dev" ]]; then echo "$PROGNAME: block device '$dev' not found" >&2 return 1 fi fi fi bklog="$ARG_OUTDIR/log/tar.$name.$ARG_LEVEL.log" # set the acls options (for mount and tar) fstype="$(get_fs_type "$dev")" acl='' acls='' if [[ "$NOACLS" != 'true' ]]; then echo "$fstype" | grep -Eq '^ext[234]$' [[ $? -eq 0 ]] && acl=',acl' acls='--acls' fi # assign the list of directories to exclude to the exclude variable # (the eval is needed because LVFSEXCLUDE[i] is a whitespace separated # list of files/directories to exclude enclosed in single quotes) # the nasty expression in ex assignment ensures that inner ' is # replaced by '"'"' in the eval so that another eval can be used # once again later in the tar command exclude='' eval 'for ex in '"${LVFSEXCLUDE[i]}"'; do ex=$(echo "$ex" | sed '"'s#'\"'\"'#'\"'\"\\\"\"'\"\\\"\"'\"'#g'"') exclude="$exclude --exclude='"'"'$ex'"'"'" done' # mount the device/volume to backup # get df log info # copy (don't overwrite) the snar archive # run tar with proper arguments, i.e. create the incremental backup log "$argv" "$PROGNAME: backing up the '$name' filesystem" \ && log "$argv" "$PROGNAME: mounting '$name', i.e. '$dev' filesystem" \ && mount -o ro,noexec,nodev,nosuid"$acl" "$dev" "$WORKDIR/mnt" \ && mounted=0 \ && if [[ "$df_fail" != 0 ]]; then log "$argv" "$PROGNAME: getting filesystem disk space usage information" \ && df -h "$WORKDIR/mnt" \ | grep -vE '^Filesystem[[:blank:]]+.*[[:blank:]]+Mounted on$' \ >>"$dflog" fi \ && if [[ "$ARG_LEVEL" -gt 0 ]]; then log "$argv" "$PROGNAME: copying snar archive level $((ARG_LEVEL-1)) to level $ARG_LEVEL" \ && cp "$ARG_OUTDIR/cache/$name.$((ARG_LEVEL-1)).snar" \ "$ARG_OUTDIR/cache/$name.$ARG_LEVEL.snar" fi \ && if [[ "${#LVFSEXCLUDE[i]}" -gt 0 ]]; then log "$argv" "$PROGNAME: excluding following files/directories from the backup" \ && eval 'for ex in '"${LVFSEXCLUDE[i]}"'; do [[ "$?" -eq 0 ]] \ && log "argv" " '"'"'$ex'"'"'" done' fi \ && log "$argv" "$PROGNAME: using tar to archive '$name' filesystem" \ && eval 'tar -cvvzf "$ARG_OUTDIR/cache/$name.$ARG_LEVEL.tgz" \ -g "$ARG_OUTDIR/cache/$name.$ARG_LEVEL.snar" \ -C "$WORKDIR/mnt" \ --sparse \ '"$acls"' \ --atime-preserve=system \ --one-file-system \ --exclude=./lost+found '"$exclude"'\ . \ >"$bklog"' # log failure and abort the backup if some operation failed if [[ "$?" != 0 ]]; then echo "$PROGNAME: last operation failed, aborting the backup" >&2 echo "$PROGNAME: backing up '$name' filesystem failed, see logs" >&2 abort=0 fi # if the processed device was mounted then unmount it if [[ "$mounted" = 0 ]]; then log "$argv" "$PROGNAME: unmounting '$name', i.e. '$dev' filesystem" if ! umount "$WORKDIR/mnt"; then echo "$PROGNAME: cannot unmount '$dev', aborting the backup," >&2 echo "$PNSPACES run umount manually" >&2 abort=0 fi fi if [[ "$abort" != 0 ]]; then log "$argv" "$PROGNAME: filesystem '$name' backed up successfully" else return 1 fi type="${LVFSTYPE[++i]}" done return 0 } # pre() # pre command mode - prepare the system for the backup, i.e. remount all # filesystems to backup read-only, create lvm snapshots and remount lvm # filesystems back read-write # return: 0 - ok # 1 - filesystems cannot be remounted ro or snapshots cannot be created pre() { local cont=0 # remount all rw lvm filesystems ro if [[ "$cont" = 0 ]]; then remount_rwlv_ro cont="$?" fi # create lvm snapshots if [[ "$cont" = 0 ]]; then create_lvsnap cont="$?" fi # remount all rw non-lvm filesystems ro - this is done as the last # operation because if lvm metadata backups are stored on an non-lvm # filesystem, they must be writeable in order to create snapshots if [[ "$cont" = 0 ]]; then remount_rwfs_ro cont="$?" fi # lvm snapshosts are created, mount the lvm filesystems back rw again if [[ "$cont" = 0 ]]; then remount_rwlv_rw else # or revert to the original state after an error [[ "${#RWFS[*]}" -gt 0 || "${#SNLV[*]}" -gt 0 || "${#RWLV[*]}" -gt 0 ]] \ && echo "$PROGNAME: error occured, reverting to the original state" >&2 [[ "${#RWFS[*]}" -gt 0 ]] && remount_rwfs_rw [[ "${#SNLV[*]}" -gt 0 ]] && remove_lvsnap [[ "${#RWLV[*]}" -gt 0 ]] && remount_rwlv_rw fi return "$((cont==0 ? 0 : 1))" } # post() # post command mode - finalize the backup, i.e. remount all non-lvm # filesystems back read-write and remove lvm snapshots # return: 0 - ok # 1 - some filesystems not remounted or some snapshots not removed post() { local ec=0 # remount all non-lvm filesystems that were mounted rw during the pre phase # and were remounted ro back rw remount_rwfs_rw_post [[ "$?" -ne 0 ]] && ec=1 # remove all snapshots of lvm logical volumes created during the pre phase remove_lvsnap_post [[ "$?" -ne 0 ]] && ec=1 return "$ec" } # run() # run the incremental backup of the level ARG_LEVEL and create logs # return: exit - all incremental backups of lower level do not exist, some # incremental backups of the same or higher level exist and # shouldn't be deleted (-f switch not given) or the deletion # of such backups or logs failed (-f switch given) # 0 - ok # 1 - backup failed run() { local ec=0 # check that all incremental backups of lower level than ARG_LEVEL exist check_previous_backups # check that no incremental backups of the same or higher level than # ARG_LEVEL exist and optionally delete them check_subsequent_backups # delete the backup logs of the same or higher level than ARG_LEVEL delete_subsequent_logs # mount, backup and unmount the filesystems, also log the backup backup [[ "$?" -ne 0 ]] && ec=1 return "$ec" } # check() level # check that all backup archives of the specified level and of all preceding # level exist # return: 0 - ok or clean exit in progress # 1 - some of the backup archives not found check() { [[ "$CE_RUNNING" = 0 ]] && return 0 # check that all incremental backups of lower level than ARG_LEVEL exist check_previous_backups return 0 } # clean() # clean command mode - perform cleanup, i.e. unmount filesystem to backup # from its backup mount point, remove lvm snapshots, remount both fs and # lvm filesystems back read-write, remove proc mounts or tmpfs filesystem # return: 0 - ok # 1 - filesystem to backup not unmounted, some snapshots not removed, # some filesystems not remounted back read-write or proc mounts # copy not removed clean() { local ec=0 # unmount backup mountpoint if mounted grep -Eq "^[[:blank:]]*[^[:blank:]]+[[:blank:]]+$WORKDIR/mnt[[:blank:]]" /proc/mounts if [[ $? -eq 0 ]]; then [[ "$ARG_V" = y ]] \ && echo "$PROGNAME: unmounting '$WORKDIR/mnt'" if ! umount "$WORKDIR/mnt"; then echo "$PROGNAME: cannot unmount '$WORKDIR/mnt'," >&2 echo "$PNSPACES run umount manually" >&2 ec=1 fi fi # remount both fs and lvm filesystems back rw if /proc/mounts copy exists check_proc_mounts 'copy' if [[ $? -eq 0 ]]; then # remount all non-lvm filesystems that were mounted rw during the pre # phase and were remounted ro back rw remount_rwfs_rw_post [[ "$?" -ne 0 ]] && ec=1 # remount all non-lvm filesystems that were mounted rw during the pre # phase and were remounted ro back rw remount_rwlv_rw_clean [[ "$?" -ne 0 ]] && ec=1 fi # remove all snapshots of lvm logical volumes created during the pre phase remove_lvsnap_post [[ "$?" -ne 0 ]] && ec=1 # unmount tmpfs filesystem if mounted umount_tmpfs [[ "$?" -ne 0 ]] && ec=1 # remove_proc_mounts remove_proc_mounts [[ "$?" -ne 0 ]] && ec=1 return "$ec" } ###### MAIN ################################################################### ### check if all the external tools are available on the system check_tools sed grep id ps cat rm mkdir mount umount true sort date cp find \ awk head readlink tr ls df tar ### parse and check arguments, run initialization parse_args "$@" check_args [[ "$ARG_MODE" = 'help' ]] && usage # check if the script is called by root check_root_id # check if the script is not running yet check_if_running # check that the working directory is all right check_work_dir "$ARG_MODE" # check that readable /proc/mounts exists check_proc_mounts 'real' # mount the tmpfs filesystem which is always read/write mount_tmpfs "$TMPFS_SIZE" # read the configuration file and check configuration afterwards read_config_file "$CONFFILE" check_config # create uuid file that maps block devices to their uuids create_uuid_file "$UUID_FILE" # check add-on tools presence check_addon_tools # default exit code EC=0 get_timestamp [[ "$ARG_MODE" != 'clean' && "$ARG_MODE" != 'check' ]] \ && add_exit_cmd 'print_result' add_exit_cmd 'get_timestamp' ### PRE if [[ "$ARG_MODE" = 'pre' ]]; then # store /proc/mounts so that it can be recognized later in the post # mode how to remount the filesystems back store_proc_mounts # main pre functionality pre if [[ "$?" -ne 0 ]]; then remove_proc_mounts EC=4 fi ### RUN elif [[ "$ARG_MODE" = 'run' ]]; then # check that a copy of /proc/mounts is available in the tmp workdir, # this indicates that this command in the pre command mode was called # successfully before check_proc_mounts 'copy' # check output directory check_outdir # main run functionality - i.e. the backup run [[ "$?" -ne 0 ]] && EC=4 ### POST elif [[ "$ARG_MODE" = 'post' ]]; then # check that the /proc/mounts file and its copy stored during # the preceding pre phase are available check_proc_mounts 'real' check_proc_mounts 'copy' # main post functionality post [[ "$?" -ne 0 ]] && EC=4 # remove_proc_mounts remove_proc_mounts [[ "$?" -ne 0 ]] && EC=4 ### CHECK elif [[ "$ARG_MODE" = 'check' ]]; then # main check functionality check [[ "$?" -ne 0 ]] && EC=4 ### CLEAN elif [[ "$ARG_MODE" = 'clean' ]]; then # main clean functionality clean [[ "$?" -ne 0 ]] && EC=4 fi ### exit [[ "$EC" = 0 ]] && RESULT='OK' clean_exit "$EC" tar-lvm/doc0000777002342000234200000000000013754042526014304 2install/doc/ustar baxicbaxictar-lvm/RELEASE-NOTES0000644002342000234200000000104213754305347013273 0ustar baxicbaxic tar-lvm-all 00.22 - output to system console added tar-lvm-one 00.22 - ssd-backup exit code not checked ssd-backup 00.22 - systemd services with argumnets supported tar-lvm-all 00.21 - ssh agent support added tar-lvm-one 00.21 - ssh agent support added ssd-backup 00.21 - bug fix - Additional characters prevented the script to run (typo). 00.80 - Initial version of the Tar-LVM suite released. bgrun 00.20 cmdsend 00.20 ssd-backup 00.20 tar-lvm 00.20 tar-lvm-one 00.20 tar-lvm-all 00.20 tar-lvm/AUTHOR0000644002342000234200000000005114041735525012321 0ustar baxicbaxicLukas Baxa alias Baxic tar-lvm/install.sh0000755002342000234200000002652114042103062013375 0ustar baxicbaxic#!/bin/bash ############################################################################### # # script: install.sh # author: Lukas Baxa alias Baxic # # This script is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # ############################################################################### # set program name PROGNAME='install.sh' PNSPACES=' ' # set versions SCRIPT_VERSION='00.22' SUITE_VERSION='00.80' # installation directories and files CHECK_DIRS=( '700:/usr/local/sbin' '700:/usr/local/etc' '700:/usr/local/share' '700:/usr/local/share/doc' '700:/var/local' ) CREATE_DIRS=( '750:/usr/local/etc/tar-lvm' '755:/usr/local/share/doc/tar-lvm' '750:/var/local/tar-lvm' '750:/var/local/tar-lvm/mnt' '750:/var/local/tar-lvm/tmp' '750:/var/local/tar-lvm/tmpfs' '750:/var/local/ssd-backup' ) INSTALL_FILES_ONE=( '755:sbin/bgrun:/usr/local/sbin/bgrun' '755:sbin/cmdsend:/usr/local/sbin/cmdsend' '755:sbin/ssd-backup:/usr/local/sbin/ssd-backup' '755:sbin/tar-lvm:/usr/local/sbin/tar-lvm' '755:sbin/tar-lvm-one:/usr/local/sbin/tar-lvm-one' '644:doc/00-backing-up-with-tar.html:/usr/local/share/doc/tar-lvm/00-backing-up-with-tar.html' '644:doc/01-overview.html:/usr/local/share/doc/tar-lvm/01-overview.html' '644:doc/02-preparing-for-the-backup.html:/usr/local/share/doc/tar-lvm/02-preparing-for-the-backup.html' '644:doc/03-installation.html:/usr/local/share/doc/tar-lvm/03-installation.html' '644:doc/04-configuration.html:/usr/local/share/doc/tar-lvm/04-configuration.html' '644:doc/05-triggering-the-backup.html:/usr/local/share/doc/tar-lvm/05-triggering-the-backup.html' '644:doc/06-restoring-the-backup.html:/usr/local/share/doc/tar-lvm/06-restoring-the-backup.html' '644:doc/07-removal.html:/usr/local/share/doc/tar-lvm/07-removal.html' '644:doc/tar-lvm-deployment-scheme.png:/usr/local/share/doc/tar-lvm/tar-lvm-deployment-scheme.png' '644:doc/tar-lvm-filesystem-prerequisities.png:/usr/local/share/doc/tar-lvm/tar-lvm-filesystem-prerequisities.png' ) CONFIG_FILES_ONE=( '600:etc/ssd-backup.conf:/usr/local/etc/ssd-backup.conf' '600:etc/tar-lvm/tar-lvm.conf:/usr/local/etc/tar-lvm/tar-lvm.conf' '600:etc/tar-lvm/tar-lvm-one.local.conf:/usr/local/etc/tar-lvm/tar-lvm-one.local.conf' ) INSTALL_FILES_ALL=( '755:sbin/tar-lvm-all:/usr/local/sbin/tar-lvm-all' ) CONFIG_FILES_ALL=( '600:etc/tar-lvm/tar-lvm-one.shared.conf:/usr/local/etc/tar-lvm/tar-lvm-one.shared.conf' '600:etc/tar-lvm/tar-lvm-all.conf:/usr/local/etc/tar-lvm/tar-lvm-all.conf' ) # check_tools() tool1 ... toolN # check if all the external tools tool1 ... toolN are available on the system # return: exit - some of the tools are missing # 0 - ok check_tools() { local fail fst local tool if ! which which >/dev/null 2>&1; then echo "$PROGNAME: cannot find the 'which' command that is necessary" >&2 exit 2 fi fail=1 fst=0 for tool in "$@"; do if ! which "$tool" >/dev/null 2>&1; then if [[ "$fst" = 0 ]]; then echo "$PROGNAME: cannot find the following tools/commands that are necessary" >&2 fst=1 fi echo " '$tool'" >&2 fail=0 fi done [[ "$fail" = 0 ]] && exit 2 return 0 } # usage() [exit_code] # print the usage info and exit with given exit code (default 0) # return: exit usage() { local ec="${1:-0}" if [[ "$ec" != 0 ]]; then exec >&2 fi echo "$PROGNAME - install tar-lvm suite" echo echo " ($PROGNAME version: $SCRIPT_VERSION, tar-lvm suite version: $SUITE_VERSION)" echo echo "usage: $PROGNAME -h | help" echo " $PROGNAME install [one|all]" echo " $PROGNAME remove" echo " $PROGNAME purge" echo echo ' -h | help ... print help and exit' echo ' install one ... install tar-lvm suite for one machine, i.e.' echo ' not tar-lvm-all script' echo ' install all ... install tar-lvm suite for all machines, i.e.' echo ' all tar-lvm scripts including tar-lvm-all' echo ' remove ... remove tar-lvm suite, keep configuration' echo ' purge ... remove tar-lvm suite including configuration' echo exit "$ec" } # parse_args() arg1 ... argN # parse the command-line arguments and set the global variables ARG_MODE # and ARG_SUBMODE # return: exit - wrong arguments are given # 0 - ok parse_args() { unset ARG_MODE unset ARG_SUBMODE [[ $# -eq 0 ]] && usage 1 if [[ "$1" = '-h' || "$1" = 'help' || "$1" = 'install' || "$1" = 'remove' || "$1" = 'purge' ]]; then ARG_MODE="$1" [[ "$ARG_MODE" = '-h' ]] && ARG_MODE='help' else usage 1 fi if [[ "$ARG_MODE" = 'install' ]]; then [[ $# -ne 2 ]] && usage 1 if [[ "$2" = 'one' || "$2" = 'all' ]]; then ARG_SUBMODE="$2" else usage 1 fi else [[ $# -ne 1 ]] && usage 1 fi return 0 } # check_root_id() # check that the command is called by root # return: exit - the command is not called by root # 0 - ok or clean exit in progress check_root_id() { if [[ $(id -u) != 0 ]]; then echo "$PROGNAME: you must be root in order to use this script" >&2 exit 2 fi return 0 } # check_dirs() # check that all required directories exist and that they have correct # permissions # return: exit - some required directory is missing or doesn't have all # required permissions # 0 - ok check_dirs() { local ec=0 local pd perm dir for pd in "${CHECK_DIRS[@]}"; do perm="$(echo "$pd" | cut -d: -f1)" dir="$(echo "$pd" | cut -d: -f2-)" if [[ ! -d "$dir" ]]; then echo "$PROGNAME: directory '$dir' not found" >&2 ec=3 continue fi if [[ "$(find "$dir" -maxdepth 0 -perm "-$perm" | wc -l)" = 0 ]]; then echo "$PROGNAME: directory '$dir' doesn't have all required" >&2 echo "$PNSPACES permission bits '$perm'" >&2 ec=3 continue fi done [[ "$ec" != 0 ]] && exit "$ec" return 0 } # create_dirs() # create all installation directories with proper permissions if they don't # exist # return: exit - cannot create installation directories with proper # permissions # 0 - ok create_dirs() { local pd perm dir declare -a arr eval 'arr=(' $(printf '%s\n' "${CREATE_DIRS[@]}" | sed -r -e 's#^#"#' -e 's#$#"#' | sort -t : -k 2) ')' for pd in "${arr[@]}"; do perm="$(echo "$pd" | cut -d: -f1)" dir="$(echo "$pd" | cut -d: -f2-)" mkdir -p -m "$perm" "$dir" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot create directory '$dir'" >&2 exit 4 fi done return 0 } # install_files() ['one'|'all'] # copy installation files into their destination and set permissions, # then copy configuration files as well if they don't exist or # to alternate files with the '.new' suffix if they already exist # return: exit - cannot copy files or set permissions # 0 - ok install_files() { local mode="$1" local psd perm src dst declare -a arr_f tmparr_f arr_c tmparr_c if [[ "$mode" = 'all' ]]; then tmparr_f=("${INSTALL_FILES_ALL[@]}") tmparr_c=("${CONFIG_FILES_ALL[@]}") fi arr_f=("${INSTALL_FILES_ONE[@]}" "${tmparr_f[@]}") arr_c=("${CONFIG_FILES_ONE[@]}" "${tmparr_c[@]}") for psd in "${arr_f[@]}"; do perm="$(echo "$psd" | cut -d: -f1)" src="$(echo "$psd" | cut -d: -f2)" dst="$(echo "$psd" | cut -d: -f3-)" cp "$RUNDIR/install/$src" "$dst" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot copy installation file: " >&2 echo "$PNSPACES - src: '$src'" >&2 echo "$PNSPACES - dst: '$dst'" >&2 exit 5 fi chmod "$perm" "$dst" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot set permissions on file '$dst'" >&2 exit 5 fi done for psd in "${arr_c[@]}"; do perm="$(echo "$psd" | cut -d: -f1)" src="$(echo "$psd" | cut -d: -f2)" dst="$(echo "$psd" | cut -d: -f3-)" if [[ ! -e "$dst" ]]; then cp "$RUNDIR/install/$src" "$dst" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot copy installation file: " >&2 echo "$PNSPACES - src: '$src'" >&2 echo "$PNSPACES - dst: '$dst'" >&2 exit 5 fi chmod "$perm" "$dst" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot set permissions on file '$dst'" >&2 exit 5 fi else cp "$RUNDIR/install/$src" "$dst.new" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot copy installation file: " >&2 echo "$PNSPACES - src: '$src'" >&2 echo "$PNSPACES - dst: '$dst.new'" >&2 exit 5 fi chmod "$perm" "$dst.new" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot set permissions on file '$dst.new'" >&2 exit 5 fi fi done return 0 } # remove_files() # remove all installed files, but keep configuration files # return: 1 - at least one file cannot be removed # 0 - ok remove_files() { local ec=0 local psd dst for psd in "${INSTALL_FILES_ONE[@]}" "${INSTALL_FILES_ALL[@]}"; do dst="$(echo "$psd" | cut -d: -f3-)" rm -f "$dst" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot remove installed file '$dst'" >&2 ec=1 fi done return "$ec" } # purge_files() # remove all configuration files including those with the '.new' suffix # return: 1 - cannot remove all configuration files # 0 - ok purge_files() { local ec=0 local psd dst for psd in "${CONFIG_FILES_ONE[@]}" "${CONFIG_FILES_ALL[@]}"; do dst="$(echo "$psd" | cut -d: -f3-)" rm -f "$dst" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot remove installed file '$dst'" >&2 ec=1 fi rm -f "$dst.new" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot remove installed file '$dst.new'" >&2 ec=1 fi done return "$ec" } # remove_dirs() # remove all installation directories if they're empty # return: 1 - cannot remove installation directories # 0 - ok remove_dirs() { local ec=0 local pd dir local cont declare -a arr eval 'arr=(' $(printf '%s\n' "${CREATE_DIRS[@]}" | sed -r -e 's#^#"#' -e 's#$#"#' | sort -t : -k 2 -r) ')' for pd in "${arr[@]}"; do dir="$(echo "$pd" | cut -d: -f2-)" if [[ -d "$dir" ]]; then cont="$(find "$dir" | sed -r "s#^#$PNSPACES - #")" if [[ "$(echo "$cont" | wc -l)" != 1 ]]; then echo "$PROGNAME: keeping directory '$dir', not empty:" >&2 echo "$cont" else rmdir "$dir" if [[ $? -ne 0 ]]; then echo "$PROGNAME: cannot remove directory '$dir'" >&2 ec=1 fi fi elif [[ -f "$dir" ]]; then echo "$PROGNAME: file '$dir' not a directory" >&2 ec=1 fi done return "$ec" } ### main check_tools id find printf sed sort mkdir cp chmod rm rmdir parse_args "$@" [[ "$ARG_MODE" = 'help' ]] && usage check_root_id EC=0 RUNDIR="$(dirname "$0")" case "$ARG_MODE" in 'install') check_dirs [[ $? -ne 0 ]] && EC=3 create_dirs [[ $? -ne 0 ]] && EC=4 install_files "$ARG_SUBMODE" [[ $? -ne 0 ]] && EC=5 ;; 'remove') remove_files [[ $? -ne 0 ]] && EC=6 remove_dirs [[ $? -ne 0 ]] && EC=8 ;; 'purge') remove_files [[ $? -ne 0 ]] && EC=6 purge_files [[ $? -ne 0 ]] && EC=7 remove_dirs [[ $? -ne 0 ]] && EC=8 ;; esac ### exit exit "$EC" tar-lvm/LICENSE0000644002342000234200000000033113754042526012404 0ustar baxicbaxicThis is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. tar-lvm/README0000644002342000234200000000020513754042526012257 0ustar baxicbaxicSee the install/doc subdirectory of the installation archive or its copy /usr/local/share/doc/tar-lvm after successful installation.