Enhanced rsync handler.
[matthijs/upstream/backupninja.git] / handlers / rsync.in
1 #
2 # backupninja handler to do incremental backups using
3 # rsync and hardlinks, based on
4 #
5 #  - http://www.mikerubel.org/computers/rsync_snapshots/
6 #  - rsnap handler by paulv at bikkel.org
7 #  - maildir handler from backupninja
8 #
9 # feedback: rhatto at riseup.net | gpl
10 #
11 # Config file options
12 # -------------------
13 #
14 #   [general]
15 #   log = rsync log file
16 #   partition = partition where the backup lives
17 #   fscheck = set to 1 if fsck should run on $partition after the backup is made
18 #   read_only = set to 1 if $partition is mounted read-only
19 #   mountpoint = backup partition mountpoint or backup main folder (either local or remote)
20 #   backupdir = folder relative do $mountpoint where the backup should be stored (local or remote)
21 #   format = specify backup storage format: short, long or mirror (i.e, no rotations)
22 #   days = for short storage format, specify the number of backup increments (min = 5)
23 #   keepdaily = for long storage format, specify the number of daily backup increments
24 #   keepweekly = for long storage format, specify the number of weekly backup increments
25 #   keepmonthly = for long storage format, specify the number of monthly backup increments
26 #   lockfile = lockfile to be kept during backup execution
27 #   nicelevel = rsync command nice level
28 #   enable_mv_timestamp_bug = set to "yes" if your system isnt handling timestamps correctly
29 #   tmp = temp folder
30 #   multiconnection = set to "yes" if you want to use multiconnection ssh support
31 #
32 #   [source]
33 #   from = local or remote
34 #   host = source hostname or ip, if remote backup
35 #   port = remote port number (remote source only)
36 #   user = remote user name (remote source only)
37 #   testconnect = when "yes", test the connection for a remote source before backup
38 #   include = include folder on backup
39 #   exclude = exclude folder on backup
40 #   ssh = ssh command line (remote source only)
41 #   protocol = ssh or rsync (remote source only)
42 #   rsync = rsync program
43 #   rsync_options = rsync command options
44 #   exclude_vserver = vserver-name (valid only if vservers = yes on backupninja.conf)
45 #   numericids = when set to 1, use numeric ids instead of user/group mappings on rsync
46 #   compress = if set to 1, compress data on rsync (remote source only)
47 #   bandwidthlimit = set a badnwidth limit in kbps (remote source only)
48 #   remote_rsync = remote rsync program (remote source only)
49 #   id_file = ssh key file (remote source only)
50 #   batch = set to "yes" to rsync use a batch file as source
51 #   batchbase = folder where the batch file is located
52 #   filelist = set yes if you want rsync to use a file list source
53 #   filelistbase = folder where the file list is placed
54 #
55 #   [dest]
56 #   dest = backup destination type (local or remote)
57 #   testconnect = when "yes", test the connection for a remote source before backup
58 #   ssh = ssh command line (remote dest only)
59 #   protocol = ssh or rsync (remote dest only)
60 #   numericids = when set to 1, use numeric ids instead of user/group mappings on rsync
61 #   compress = if set to 1, compress data on rsync (remote source only)
62 #   host = destination host name (remote destination only)
63 #   port = remote port number (remote destination only)
64 #   user = remote user name (remote destination only)
65 #   id_file = ssh key file (remote destination only)
66 #   bandwidthlimit = set a badnwidth limit in kbps (remote destination only)
67 #   remote_rsync = remote rsync program (remote dest only)
68 #   batch = set to "yes" to rsync write a batch file from the changes
69 #   batchbase = folder where the batch file should be written
70 #   fakesuper = set to yes so rsync use the --fake-super flag (remote destination only)
71 #
72 #   [services]
73 #   initscripts = absolute path where scripts are located
74 #   service = script name to be stoped at the begining of the backup and started at its end
75 #
76 # You can also specify some system comands if you don't want the default system values:
77 #
78 #   [system]
79 #   rm = rm command
80 #   cp = cp command
81 #   touch = touch command
82 #   mv = mv command
83 #   fsck = fsck command
84 #
85 # You dont need to manually specify vservers using "include = /vservers".
86 # They are automatically backuped if vserver is set to "yes" on you backupninja.conf.
87 #
88 # Changelog
89 # ---------
90
91 # 20090329 - rhatto at riseup.net
92 #
93 #   - Added support for: 
94 #     - Remote destinations
95 #     - Long rotation format similar to maildir handler
96 #     - Batch files through --read-batch and --write-batch
97 #     - Custom file list using --files-from
98 #     - SSH persistent connection using ControlMaster
99 #     - The rsync:// protocol
100 #   - Metadata folder for each backup folder
101 #   - General refactoring
102 #   - Code cleanup
103 #
104
105 # function definitions
106
107 function eval_config {
108   
109   # system section
110   
111   setsection system
112   getconf rm rm
113   getconf cp cp
114   getconf touch touch
115   getconf mv mv
116   getconf fsck fsck
117   
118   # general section
119   
120   setsection general
121   getconf log /var/log/backup/rsync.log
122   getconf partition
123   getconf fscheck
124   getconf read_only
125   getconf mountpoint
126   getconf backupdir
127   getconf format short
128   getconf days
129   getconf keepdaily 5
130   getconf keepweekly 3
131   getconf keepmonthly 1
132   getconf lockfile
133   getconf nicelevel 0
134   getconf enable_mv_timestamp_bug no
135   getconf tmp /tmp
136   getconf multiconnection no
137   
138   # source section
139   
140   setsection source
141   getconf from local
142   getconf rsync $RSYNC
143   getconf rsync_options "-av --delete --recursive"
144   
145   if [ "$from" == "remote" ]; then
146     getconf testconnect no
147     getconf protocol ssh
148     getconf ssh ssh
149     getconf host
150
151     if [ "$protocol" == "ssh" ]; then
152       # sshd default listen port
153       getconf port 22
154     else
155       # rsyncd default listen port
156       getconf port 873
157     fi
158
159     getconf user
160     getconf bandwidthlimit
161     getconf remote_rsync rsync
162     getconf id_file /root/.ssh/id_dsa
163   fi
164   
165   getconf batch no
166
167   if [ "$batch" == "yes" ]; then
168     getconf batchbase
169     if [ ! -z "$batchbase" ]; then
170       batch="read"
171     fi
172   fi
173
174   getconf filelist no
175   getconf filelistbase
176   getconf include
177   getconf exclude
178   getconf exclude_vserver
179   getconf numericids 0
180   getconf compress 0
181   
182   # dest section
183   
184   setsection dest
185   getconf dest local
186   getconf fakesuper no
187   
188   if [ "$dest" == "remote" ]; then
189     getconf testconnect no
190     getconf protocol ssh
191     getconf ssh ssh
192     getconf host
193
194     if [ "$protocol" == "ssh" ]; then
195       # sshd default listen port
196       getconf port 22
197     else
198       # rsyncd default listen port
199       getconf port 873
200     fi
201
202     getconf user
203     getconf bandwidthlimit
204     getconf remote_rsync rsync
205     getconf id_file /root/.ssh/id_dsa
206   fi
207   
208   getconf batch no
209
210   if [ "$batch" != "yes" ]; then
211     getconf batch no
212     if [ "$batch" == "yes" ]; then
213       getconf batchbase
214       if [ ! -z "$batchbase" ]; then
215         batch="write"
216       fi
217     fi
218   fi
219
220   getconf numericids 0
221   getconf compress 0
222   
223   # services section
224   
225   setsection services
226   getconf initscripts /etc/init.d
227   getconf service
228
229   # config check
230
231   if [ "$dest" != "local" ] && [ "$from" == "remote" ]; then
232     fatal "When source is remote, destination should be local."
233     exit 1
234   fi
235
236   if [ "$from" != "local" ] && [ "$from" != "remote" ]; then
237     fatal "Invalid source $from"
238     exit 1
239   fi
240
241   backupdir="$mountpoint/$backupdir"
242
243   if [ "$dest" == "local" ] && [ ! -d "$backupdir" ]; then 
244     error "Backupdir $backupdir does not exist"
245     exit 1
246   fi
247
248   if [ ! -z "$log" ]; then
249     mkdir -p `dirname $log`
250   fi
251
252   if [ "$format" == "short" ]; then
253     if [ -z "$days" ]; then
254       keep="4"
255     else
256       keep="`echo $days - 1 | bc -l`"
257     fi
258   fi
259
260   if [ ! -z "$nicelevel" ]; then 
261     nice="nice -n $nicelevel"
262   else 
263     nice=""
264   fi
265
266   ssh_cmd="ssh -T -o PasswordAuthentication=no $host -p $port -l $user -i $id_file"
267
268   if [ "$from" == "remote" ] || [ "$dest" == "remote" ]; then
269     if [ "$testconnect" == "yes" ] && [ "$protocol" == "ssh" ]; then
270       test_connect $host $port $user $id_file
271     fi
272   fi
273
274   if [ "$multiconnection" == "yes" ]; then
275     ssh_cmd="$ssh_cmd -S $tmp/%r@%h:%p"
276   fi
277
278   if [ $enable_mv_timestamp_bug == "yes" ]; then
279     mv=move_files
280   fi
281
282   for path in $exclude; do
283     excludes="$excludes --exclude=$path"
284   done
285
286 }
287
288 function rotate_short {
289
290   local dest
291   local folder="$1"
292   local keep="$2"
293   local metadata="`dirname $folder`/metadata"
294
295   if [[ "$keep" < 4 ]]; then
296     error "Rotate: minimum of 4 rotations"
297     exit 1
298   fi
299
300   if [ -d $folder.$keep ]; then
301     $nice $mv /$folder.$keep /$folder.tmp
302   fi
303
304   for ((n=`echo "$keep - 1" | bc`; n >= 0; n--)); do
305     if [ -d $folder.$n ]; then
306       dest=`echo "$n + 1" | bc`
307       $nice $mv /$folder.$n /$folder.$dest
308       $touch /$folder.$dest
309       mkdir -p $metadata/`basename $folder`.$dest
310       date +%c%n%s > $metadata/`basename $folder`.$dest/rotated
311     fi
312   done
313
314   if [ -d $folder.tmp ]; then
315     $nice $mv /$folder.tmp /$folder.0
316   fi
317
318   if [ -d $folder.1 ]; then
319     $nice $cp -alf /$folder.1/. /$folder.0
320   fi
321
322 }
323
324 function rotate_short_remote {
325
326   local folder="$1"
327   local metadata="`dirname $folder`/metadata"
328   local keep="$2"
329
330   if [[ "$2" < 4 ]]; then
331     error "Rotate: minimum of 4 rotations"
332     exit 1
333   fi
334
335 (
336   $ssh_cmd <<EOF
337   ##### BEGIN REMOTE SCRIPT #####
338
339   if [ -d $folder.$keep ]; then
340     $nice mv /$folder.$keep /$folder.tmp
341   fi
342
343   for ((n=$(($keep - 1)); n >= 0; n--)); do
344     if [ -d $folder.\$n ]; then
345       dest=\$((\$n + 1))
346       $nice mv /$folder.\$n /$folder.\$dest
347       touch /$folder.\$dest
348       mkdir -p $metadata/`basename $folder`.\$dest
349       date +%c%n%s > $metadata/`basename $folder`.\$dest/rotated
350     fi
351   done
352
353   if [ -d $folder.tmp ]; then
354     $nice mv /$folder.tmp /$folder.0
355   fi
356
357   if [ -d $folder.1 ]; then
358     $nice $cp -alf /$folder.1/. /$folder.0
359   fi
360   ##### END REMOTE SCRIPT #######
361 EOF
362 ) | (while read a; do passthru $a; done)
363
364 }
365
366 function rotate_long {
367
368   backuproot="$1"
369   seconds_daily=86400
370   seconds_weekly=604800
371   seconds_monthly=2628000
372   keepdaily=$keepdaily
373   keepweekly=$keepweekly
374   keepmonthly=$keepmonthly
375   now=`date +%s`
376
377   local metadata
378
379   if [ ! -d "$backuproot" ]; then
380     echo "Debug: skipping rotate of $backuproot as it doesn't exist."
381     exit
382   fi
383
384   for rottype in daily weekly monthly; do
385     seconds=$((seconds_${rottype}))
386
387     dir="$backuproot/$rottype"
388     metadata="$backuproot/metadata/$rottype.1"
389     mkdir -p $metadata
390     if [ ! -d $dir.1 ]; then
391       echo "Debug: $dir.1 does not exist, skipping."
392       continue 1
393     elif [ ! -f $metadata/created ] && [ ! -f $metadata/rotated ]; then
394       echo "Warning: metadata does not exist for $dir.1. This backup may be only partially completed. Skipping rotation."
395       continue 1
396     fi
397     
398     # Rotate the current list of backups, if we can.
399     oldest=`find $backuproot -maxdepth 1 -type d -name $rottype'.*' | @SED@ 's/^.*\.//' | sort -n | tail -1`
400     [ "$oldest" == "" ] && oldest=0
401     for (( i=$oldest; i > 0; i-- )); do
402       if [ -d $dir.$i ]; then
403         if [ -f $metadata/created ]; then
404           created=`tail -1 $metadata/created`
405         elif [ -f $metadata/rotated ]; then
406           created=`tail -1 $metadata/rotated`
407         else
408           created=0
409         fi
410         cutoff_time=$(( now - (seconds*(i-1)) ))
411         if [ ! $created -gt $cutoff_time ]; then
412           next=$(( i + 1 ))
413           if [ ! -d $dir.$next ]; then
414             echo "Debug: $rottype.$i --> $rottype.$next"
415             $nice mv $dir.$i $dir.$next
416             mkdir -p $backuproot/metadata/$rottype.$next
417             date +%c%n%s > $backuproot/metadata/$rottype.$next/rotated
418           else
419             echo "Debug: skipping rotation of $dir.$i because $dir.$next already exists."
420           fi
421         else
422           echo "Debug: skipping rotation of $dir.$i because it was created" $(( (now-created)/86400)) "days ago ("$(( (now-cutoff_time)/86400))" needed)."
423         fi
424       fi
425     done
426   done
427
428   max=$((keepdaily+1))
429   if [ $keepweekly -gt 0 -a -d $backuproot/daily.$max -a ! -d $backuproot/weekly.1 ]; then
430     echo "Debug: daily.$max --> weekly.1"
431     $nice mv $backuproot/daily.$max $backuproot/weekly.1
432     mkdir -p $backuproot/metadata/weekly.1
433     date +%c%n%s > $backuproot/metadata/weekly.1/rotated
434   fi
435
436   max=$((keepweekly+1))
437   if [ $keepmonthly -gt 0 -a -d $backuproot/weekly.$max -a ! -d $backuproot/monthly.1 ]; then
438     echo "Debug: weekly.$max --> monthly.1"
439     $nice mv $backuproot/weekly.$max $backuproot/monthly.1
440     mkdir -p $backuproot/metadata/monthly.1
441     date +%c%n%s > $backuproot/metadata/monthly.1/rotated
442   fi
443
444   for rottype in daily weekly monthly; do
445     max=$((keep${rottype}+1))
446     dir="$backuproot/$rottype"
447     oldest=`find $backuproot -maxdepth 1 -type d -name $rottype'.*' | @SED@ 's/^.*\.//' | sort -n | tail -1`
448     [ "$oldest" == "" ] && oldest=0 
449     # if we've rotated the last backup off the stack, remove it.
450     for (( i=$oldest; i >= $max; i-- )); do
451       if [ -d $dir.$i ]; then
452         if [ -d $backuproot/rotate.tmp ]; then
453           echo "Debug: removing rotate.tmp"
454           $nice rm -rf $backuproot/rotate.tmp
455         fi
456         echo "Debug: moving $rottype.$i to rotate.tmp"
457         $nice mv $dir.$i $backuproot/rotate.tmp
458       fi
459     done
460   done
461
462 }
463
464 function rotate_long_remote {
465
466   local backuproot="$1"
467
468 (
469   $ssh_cmd <<EOF
470   ##### BEGIN REMOTE SCRIPT #####
471
472   seconds_daily=86400
473   seconds_weekly=604800
474   seconds_monthly=2628000
475   keepdaily=$keepdaily
476   keepweekly=$keepweekly
477   keepmonthly=$keepmonthly
478   now=\`date +%s\`
479
480   if [ ! -d "$backuproot" ]; then
481     echo "Debug: skipping rotate of $backuproot as it doesn't exist."
482     exit
483   fi
484
485   for rottype in daily weekly monthly; do
486     seconds=\$((seconds_\${rottype}))
487
488     dir="$backuproot/\$rottype"
489     metadata="$backuproot/metadata/\$rottype.1"
490     mkdir -p \$metadata
491     if [ ! -d \$dir.1 ]; then
492       echo "Debug: \$dir.1 does not exist, skipping."
493       continue 1
494     elif [ ! -f \$metadata/created ] && [ ! -f \$metadata/rotated ]; then
495       echo "Warning: metadata does not exist for \$dir.1. This backup may be only partially completed. Skipping rotation."
496       continue 1
497     fi
498     
499     # Rotate the current list of backups, if we can.
500     oldest=\`find $backuproot -maxdepth 1 -type d -name \$rottype'.*' | @SED@ 's/^.*\.//' | sort -n | tail -1\`
501     [ "\$oldest" == "" ] && oldest=0
502     for (( i=\$oldest; i > 0; i-- )); do
503       if [ -d \$dir.\$i ]; then
504         if [ -f \$metadata/created ]; then
505           created=\`tail -1 \$metadata/created\`
506         elif [ -f \$metadata/rotated ]; then
507           created=\`tail -1 \$metadata/rotated\`
508         else
509           created=0
510         fi
511         cutoff_time=\$(( now - (seconds*(i-1)) ))
512         if [ ! \$created -gt \$cutoff_time ]; then
513           next=\$(( i + 1 ))
514           if [ ! -d \$dir.\$next ]; then
515             echo "Debug: \$rottype.\$i --> \$rottype.\$next"
516             $nice mv \$dir.\$i \$dir.\$next
517             mkdir -p $backuproot/metadata/\$rottype.\$next
518             date +%c%n%s > $backuproot/metadata/\$rottype.\$next/rotated
519           else
520             echo "Debug: skipping rotation of \$dir.\$i because \$dir.\$next already exists."
521           fi
522         else
523           echo "Debug: skipping rotation of \$dir.\$i because it was created" \$(( (now-created)/86400)) "days ago ("\$(( (now-cutoff_time)/86400))" needed)."
524         fi
525       fi
526     done
527   done
528
529   max=\$((keepdaily+1))
530   if [ \$keepweekly -gt 0 -a -d $backuproot/daily.\$max -a ! -d \$backuproot/weekly.1 ]; then
531     echo "Debug: daily.\$max --> weekly.1"
532     $nice mv $backuproot/daily.\$max $backuproot/weekly.1
533     mkdir -p $backuproot/metadata/weekly.1
534     date +%c%n%s > $backuproot/metadata/weekly.1/rotated
535   fi
536
537   max=\$((keepweekly+1))
538   if [ \$keepmonthly -gt 0 -a -d $backuproot/weekly.\$max -a ! -d $backuproot/monthly.1 ]; then
539     echo "Debug: weekly.\$max --> monthly.1"
540     $nice mv $backuproot/weekly.\$max $backuproot/monthly.1
541     mkdir -p $backuproot/metadata/monthly.1
542     date +%c%n%s > $backuproot/metadata/monthly.1/rotated
543   fi
544
545   for rottype in daily weekly monthly; do
546     max=\$((keep\${rottype}+1))
547     dir="$backuproot/\$rottype"
548     oldest=\`find $backuproot -maxdepth 1 -type d -name \$rottype'.*' | @SED@ 's/^.*\.//' | sort -n | tail -1\`
549     [ "\$oldest" == "" ] && oldest=0 
550     # if we've rotated the last backup off the stack, remove it.
551     for (( i=\$oldest; i >= \$max; i-- )); do
552       if [ -d \$dir.\$i ]; then
553         if [ -d $backuproot/rotate.tmp ]; then
554           echo "Debug: removing rotate.tmp"
555           $nice rm -rf $backuproot/rotate.tmp
556         fi
557         echo "Debug: moving \$rottype.\$i to rotate.tmp"
558         $nice mv \$dir.\$i $backuproot/rotate.tmp
559       fi
560     done
561   done
562   ##### END REMOTE SCRIPT #######
563 EOF
564 ) | (while read a; do passthru $a; done)
565
566 }
567
568 function setup_long_dirs {
569
570   local destdir=$1
571   local backuptype=$2
572   local dir="$destdir/$backuptype"
573   local tmpdir="$destdir/rotate.tmp"
574   local metadata="$destdir/metadata/$backuptype.1"
575
576   if [ ! -d $destdir ]; then
577     echo "Creating destination directory $destdir..."
578     mkdir -p $destdir
579   fi
580
581   if [ -d $dir.1 ]; then
582     if [ -f $metadata/created ]; then
583       echo "Warning: $dir.1 already exists. Overwriting contents."
584     else
585       echo "Warning: we seem to be resuming a partially written $dir.1"
586     fi
587   else
588     if [ -d $tmpdir ]; then
589       mv $tmpdir $dir.1
590       if [ $? == 1 ]; then
591         echo "Fatal: could mv $destdir/rotate.tmp $dir.1 on host $host"
592         exit 1
593       fi
594     else
595       mkdir --parents $dir.1
596       if [ $? == 1 ]; then
597         echo "Fatal: could not create directory $dir.1 on host $host"
598         exit 1
599       fi
600     fi
601     if [ -d $dir.2 ]; then
602       echo "Debug: update links $backuptype.2 --> $backuptype.1"
603       cp -alf $dir.2/. $dir.1
604       #if [ $? == 1 ]; then
605       #  echo "Fatal: could not create hard links to $dir.1 on host $host"
606       #  exit 1
607       #fi
608     fi
609   fi
610   [ -f $metadata/created ] && rm $metadata/created
611   [ -f $metadata/rotated ] && rm $metadata/rotated
612
613 }
614
615 function setup_long_dirs_remote {
616
617   local destdir=$1
618   local backuptype=$2
619   local dir="$destdir/$backuptype"
620   local tmpdir="$destdir/rotate.tmp"
621   local metadata="$destdir/metadata/$backuptype.1"
622
623 (
624   $ssh_cmd <<EOF
625   ##### BEGIN REMOTE SCRIPT #####
626   if [ ! -d $destdir ]; then
627     echo "Creating destination directory $destdir on $host..."
628     mkdir -p $destdir
629   fi
630
631   if [ -d $dir.1 ]; then
632     if [ -f $metadata/created ]; then
633       echo "Warning: $dir.1 already exists. Overwriting contents."
634     else
635       echo "Warning: we seem to be resuming a partially written $dir.1"
636     fi
637   else
638     if [ -d $tmpdir ]; then
639       mv $tmpdir $dir.1
640       if [ \$? == 1 ]; then
641         echo "Fatal: could mv $destdir/rotate.tmp $dir.1 on host $host"
642         exit 1
643       fi
644     else
645       mkdir --parents $dir.1
646       if [ \$? == 1 ]; then
647         echo "Fatal: could not create directory $dir.1 on host $host"
648         exit 1
649       fi
650     fi
651     if [ -d $dir.2 ]; then
652       echo "Debug: update links $backuptype.2 --> $backuptype.1"
653       cp -alf $dir.2/. $dir.1
654       #if [ \$? == 1 ]; then
655       #  echo "Fatal: could not create hard links to $dir.1 on host $host"
656       #  exit 1
657       #fi
658     fi
659   fi
660   [ -f $metadata/created ] && rm $metadata/created
661   [ -f $metadata/rotated ] && rm $metadata/rotated
662   ##### END REMOTE SCRIPT #######
663 EOF
664 ) | (while read a; do passthru $a; done)
665
666 }
667
668 function move_files {
669
670   ref=$tmp/makesnapshot-mymv-$$;
671   $touch -r $1 $ref;
672   $mv $1 $2;
673   $touch -r $ref $2;
674   $rm $ref;
675
676 }
677
678 function prepare_storage {
679
680   section="`basename $SECTION`"
681
682   if [ "$format" == "short" ]; then
683
684     suffix="$section.0"
685     info "Rotating $backupdir/$SECTION..."
686     echo "Rotating $backupdir/$SECTION..." >> $log
687
688     if [ "$dest" == "remote" ]; then
689       rotate_short_remote $backupdir/$SECTION/$section $keep
690     else
691       rotate_short $backupdir/$SECTION/$section $keep
692       if [ ! -d "$backupdir/$SECTION/$section.0" ]; then
693         mkdir -p $backupdir/$SECTION/$section.0
694       fi
695     fi
696
697   elif [ "$format" == "long" ]; then
698
699     if [ $keepdaily -gt 0 ]; then
700       btype=daily
701     elif [ $keepweekly -gt 0 ]; then
702       btype=weekly
703     elif [ $keepmonthly -gt 0 ]; then
704       btype=monthly
705     else
706       fatal "keeping no backups";
707       exit 1
708     fi
709
710     suffix="$btype.1"
711     info "Rotating $backupdir/$SECTION/..."
712     echo "Rotating $backupdir/$SECTION/..." >> $log
713
714     if [ "$dest" == "remote" ]; then
715       rotate_long_remote $backupdir/$SECTION
716       setup_long_dirs_remote $backupdir/$SECTION $btype
717     else
718       rotate_long $backupdir/$SECTION
719       setup_long_dirs $backupdir/$SECTION $btype
720     fi
721
722   elif [ "$format" == "mirror" ]; then
723     suffix=""
724   else
725     fatal "Invalid backup format $format"
726     exit 1
727   fi
728
729 }
730
731 function set_orig {
732
733   if [ "$from" == "local" ]; then
734     orig="/$SECTION/"
735   elif [ "$from" == "remote" ]; then
736     if [ "$protocol" == "rsync" ]; then
737       orig="rsync://$user@$host:$port/$SECTION/"
738     else
739       orig="$user@$host:/$SECTION/"
740     fi
741   fi
742
743 }
744
745 function set_dest { 
746
747   if [ "$dest" == "local" ]; then
748     dest_path="$backupdir/$SECTION/$suffix/"
749   else
750     if [ "$protocol" == "rsync" ]; then
751       dest_path="rsync://$user@$host:$port/$backupdir/$SECTION/$suffix/"
752     else
753       dest_path="$user@$host:$backupdir/$SECTION/$suffix/"
754     fi
755   fi
756
757 }
758
759 function set_batch_mode {
760
761   local batch_file="$batchbase/$SECTION/$suffix"
762
763   if [ "$batch" == "read" ]; then
764     if [ -e "$batch_file" ]; then
765       orig=""
766       excludes=""
767       batch_option="--read-batch=$batch_file"
768     else
769       fatal "Batch file not found: $batch_file"
770       exit 1
771     fi
772   elif [ "$batch" == "write" ]; then
773     mkdir -p `dirname $batch_file`
774     batch_option="--write-batch=$batch_file"
775   fi
776
777 }
778
779 function update_metadata {
780
781   local metadata
782   local folder
783
784   if [ "$dest" == "local" ]; then
785     metadata="`dirname $dest_path`/metadata/`basename $dest_path`"
786     mkdir -p $metadata
787     date +%c%n%s > $metadata/created
788     $touch $backupdir/$SECTION/$suffix
789   else
790     folder="`echo $dest_path | cut -d : -f 2`"
791     metadata="`dirname $folder`/metadata/`basename $folder`"
792
793 (
794   $ssh_cmd <<EOF
795     ##### BEGIN REMOTE SCRIPT #####
796     mkdir -p $metadata
797     date +%c%n%s > $metadata/created
798     ##### END REMOTE SCRIPT #######
799 EOF
800 ) | (while read a; do passthru $a; done)
801
802   fi
803
804 }
805
806 function test_connect {
807
808   local host="$1"
809   local port="$2"
810   local user="$3"
811   local id_file="$4"
812
813   if [ -z "$host" ] || [ -z "$user" ]; then
814     fatal "Remote host or user not set"
815     exit 1
816   fi
817
818   debug "$ssh_cmd 'echo -n 1'"
819   result=`$ssh_cmd 'echo -n 1'`
820
821   if [ "$result" != "1" ]; then
822     fatal "Can't connect to $host as $user."
823     exit 1
824   else
825     debug "Connected to $host successfully"
826   fi
827
828 }
829
830 function set_lockfile {
831
832   if [ ! -z "$lockfile" ]; then
833     $touch $lockfile || warning "Could not create lockfile $lockfile"
834   fi
835
836 }
837
838 function unset_lockfile {
839
840   if [ ! -z "$lockfile" ]; then
841     $rm $lockfile || warning "Could not remove lockfile $lockfile"
842   fi
843
844 }
845
846 function set_filelist {
847
848   filelist_flag=""
849
850   if [ "$filelist" == "yes" ]; then
851     if [ ! -z "$filelistbase" ]; then
852       if [ -e "$filelistbase/$SECTION/$suffix" ]; then
853         filelist_flag="--files-from=$filelistbase/$SECTION/$suffix"
854       else
855         warning "File list $filelistbase/$SECTION/$suffix not found."
856       fi
857     else
858       warning "No filelistbase set."
859     fi
860   fi
861
862 }
863
864 function set_rsync_options {
865
866   if [ ! -z "$numericids" ]; then
867     rsync_options="$rsync_options --numeric-ids"
868   fi
869
870   if [ "$from" == "local" ] || [ "$dest" == "local" ]; then
871     # rsync options for local sources or destinations
872     rsync_options="$rsync_options"
873   fi
874
875   if [ "$from" == "remote" ] || [ "$dest" == "remote" ]; then
876
877     # rsync options for remote sources or destinations
878
879     if [ "$compress" == "1" ]; then
880       rsync_options="$rsync_options --compress"
881     fi
882
883     if [ ! -z "$bandwidthlimit" ]; then
884       rsync_options="$rsync_options --bwlimit=$bandwidthlimit"
885     fi
886     
887     if [ "$fakesuper" == "yes" ]; then
888       remote_rsync="$remote_rsync --fake-super"
889     fi
890
891     rsync_options=($rsync_options --rsync-path="$remote_rsync")
892
893     if [ "$protocol" == "ssh" ]; then
894       if [ ! -e "$id_file" ]; then
895         fatal "SSH Identity file $id_file not found"
896         exit 1
897       else
898         debug RSYNC_RSH=\"$ssh_cmd\"
899         echo RSYNC_RSH=\"$ssh_cmd\" >> $log
900         RSYNC_RSH="$ssh_cmd"
901       fi
902     fi
903
904   fi
905
906   include_vservers
907
908 }
909
910 function stop_services {
911
912   if [ ! -z "$service" ]; then
913     for daemon in $service; do
914       info "Stopping service $daemon..."
915       $initscripts/$daemon stop
916     done
917   fi
918
919 }
920
921 function start_services {
922
923   # restart services
924
925   if [ ! -z "$service" ]; then
926     for daemon in $service; do
927       info "Starting service $daemon..."
928       $initscripts/$daemon start
929     done
930   fi
931
932 }
933
934 function mount_rw {
935
936   # mount backup destination folder as read-write
937
938   if [ "$dest" == "local" ]; then
939     if [ "$read_only" == "1" ] || [ "$read_only" == "yes" ]; then
940       if [ -d "$mountpoint" ]; then
941         mount -o remount,rw $mountpoint
942         if (($?)); then
943           error "Could not mount $mountpoint"
944           exit 1
945         fi
946       fi
947     fi
948   fi
949
950 }
951
952 function mount_ro {
953
954   # remount backup destination as read-only
955
956   if [ "$dest" == "local" ]; then
957     if [ "$read_only" == "1" ] || [ "$read_only" == "yes" ]; then
958       mount -o remount,ro $mountpoint
959     fi
960   fi
961
962 }
963
964 function run_fsck {
965
966   # check partition for errors
967
968   if [ "$dest" == "local" ]; then
969     if [ "$fscheck" == "1" ] || [ "$fscheck" == "yes" ]; then
970       umount $mountpoint
971       if (($?)); then
972         warning "Could not umount $mountpoint to run fsck"
973       else
974         $nice $fsck -v -y $partition >> $log
975         mount $mountpoint
976       fi
977     fi
978   fi
979
980 }
981
982 function include_vservers {
983
984   # add vservers to included folders
985
986   if [ "$vservers_are_available" == "yes" ]; then
987
988     # sane permission on backup
989     mkdir -p $backupdir/$VROOTDIR
990     chmod 000 $backupdir/$VROOTDIR
991
992     for candidate in $found_vservers; do
993       candidate="`basename $candidate`"
994       found_excluded_vserver="0"
995       for excluded_vserver in $exclude_vserver; do
996         if [ "$excluded_vserver" == "$candidate" ]; then
997           found_excluded_vserver="1"
998           break
999         fi
1000       done
1001       if [ "$found_excluded_vserver" == "0" ]; then
1002         include="$include $VROOTDIR/$candidate"
1003       fi
1004     done
1005   fi
1006
1007 }
1008
1009 function start_mux {
1010
1011   if [ "$multiconnection" == "yes" ]; then
1012     debug "Starting master ssh connection"
1013     $ssh_cmd -M sleep 1d &
1014     sleep 1
1015   fi
1016
1017 }
1018
1019 function end_mux {
1020
1021   if [ "$multiconnection" == "yes" ]; then
1022     debug "Stopping master ssh connection"
1023     $ssh_cmd pkill sleep
1024   fi
1025
1026 }
1027
1028 # the backup procedure
1029
1030 eval_config
1031 set_lockfile
1032 set_rsync_options
1033 start_mux
1034 stop_services
1035 mount_rw
1036
1037 echo "Starting backup at `date`" >> $log
1038
1039 for SECTION in $include; do
1040
1041   prepare_storage
1042   set_orig
1043   set_batch_mode
1044   set_filelist
1045   set_dest
1046
1047   info "Syncing $SECTION on $dest_path..."
1048   debug $nice $rsync "${rsync_options[@]}" $filelist_flag $excludes $batch_option $orig $dest_path
1049   $nice $rsync "${rsync_options[@]}" $filelist_flag $excludes $batch_option $orig $dest_path >> $log
1050
1051   if [ "$?" != "0" ]; then
1052     warning "Rsync error when trying to transfer $SECTION"
1053   fi
1054
1055   update_metadata
1056
1057 done
1058
1059 mount_ro
1060 run_fsck
1061 start_services
1062 unset_lockfile
1063 end_mux
1064
1065 echo "Finnishing backup at `date`" >> $log
1066