1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155 declare -r PROGNAME=$(basename $0)
156 declare -r VERSION="0.3"
157
158 declare -r TEMP_DIR=/tmp
159 declare -r CONF_DIR=/etc/backup
160 declare -r CONF_FILE=$CONF_DIR/conf
161 declare -r LOCATIONS_FILE=$CONF_DIR/locations
162 declare -r FTP_CONF_FILE=$CONF_DIR/ftp
163
164 declare -r PIDFILE_DIR=/var/run
165 declare -r PIDFILE=$PIDFILE_DIR/$PROGNAME.pid
166
167 declare -r CURDATE=$(date --iso-8601)
168
169
170
171 declare -r BACKUP_SUFFIX=".$CURDATE.tar.gz"
172 declare -r PACKAGE_LIST_SUFFIX=".$CURDATE.gz"
173
174 declare -i MAX_COPIES=10
175
176
177
178
179
180
181
182
183 function clean_up() {
184
185
186
187
188
189
190 rm -f -r ${TEMP_DIR1}
191 rm -f ${PIDFILE}
192 return
193
194 }
195
196
197 function error_exit() {
198
199
200
201
202
203
204
205
206 echo "${PROGNAME}: ${1:-"Unknown Error."}" >&2
207 if [ -n "$MAILTO" ]; then
208 echo "${PROGNAME}: ${1:-"Unknown Error."}" | \
209 mail -s "Backup fault!" ${MAILTO}
210 fi
211 clean_up
212 exit 1
213
214 }
215
216
217 function graceful_exit() {
218
219
220
221
222
223
224 clean_up
225 exit
226
227 }
228
229
230 function signal_exit() {
231
232
233
234
235
236
237
238 case $1 in
239 INT)
240 echo "$PROGNAME: Program aborted by user." >&2
241 clean_up
242 exit
243 ;;
244 TERM)
245 echo "$PROGNAME: Program terminated." >&2
246 clean_up
247 exit
248 ;;
249 *)
250 error_exit "$PROGNAME: Terminating on unknown signal."
251 ;;
252
253 esac
254
255 }
256
257
258 function make_temp_files() {
259
260
261
262
263
264
265
266
267
268
269 TEMP_DIR1=$(mktemp -d -q "${TEMP_DIR}/${PROGNAME}.$$.XXXXXX")
270 if [ "$TEMP_DIR1" = "" ]; then
271 error_exit "Cannot create temp dir!"
272 fi
273 TEMP_FILE1=$(mktemp -q "${TEMP_DIR1}/${PROGNAME}.$$.XXXXXX")
274 if [ "$TEMP_FILE1" = "" ]; then
275 error_exit "Cannot create temp file!"
276 fi
277
278 }
279
280
281 function usage() {
282
283
284
285
286
287
288 echo "Usage: ${PROGNAME} [-h | --help]"
289
290 }
291
292
293 function helptext() {
294
295
296
297
298
299
300 cat <<EOF
301
302 ${PROGNAME} ${VERSION}
303
304 This is a program to backup specified directories as gzip'ed tar
305 archives on certain days on a ftp server, keeping a number of
306 older archives of the directories there (max is 10) and removing
307 superfluous ones. Additionally, the list of packages on a Debian
308 system is archived.
309
310 The name, user and password of the ftp server, the directories to
311 backup, the days of the week on which a backup is created, the
312 number of old copies and package lists (if any) to keep can be
313 specified in three configuration files located in "/etc/backup"
314 described below. On error (all errors are considered fatal and
315 will lead this program to abort) mail can be send to an
316 administrator.
317
318 Samples of the three configuration files 'ftp', 'locations' and
319 'config' sourced and parsed by this script are given here:
320
321 ftp: stores the information this script needs to login into remote
322 FTP server specified here.
323
324 --------------------------------
325 HOST="ftp.server.org"
326 USER=zippy"
327 PASSWORD="ThePinHead"
328 --------------------------------
329
330 locations: specifies which directories to backup on which days of
331 the week, how the backup archive should be called and
332 how many old backup archives should be kept on the FTP
333 server.
334
335 --------------------------------
336 # This files specifies the directories to backup, the days-of-week
337 # setting is a comma separated list of integers from 1 to 7
338 # indicating the days of the week (1 corresponding to Monday) that
339 # directory should be backed up. Name is the basename of the
340 # filename to use for the resulting archive, leading to filenames
341 # of the form "Name.YY-MM-DD.tar.gz". The last value specifies
342 # the number of archives to keep on the ftp server (>0,
343 # <=10). Additional (older) copies will be deleted!
344
345 # directory days-of-week name copies to keep
346 /home 1,2,3,4,5,6,7 home 3
347 /var/lib 1,2,3,4,5,6,7 var.lib 1
348 /var/www 1 var.www 7
349 /etc 1,3 etc 9
350 /root 1 root 10
351 --------------------------------
352
353 conf: specifies mail address of backup administrator in case of an
354 error and if (on a Debian system) the list of installed
355 packages should be backed up, too.
356
357 --------------------------------
358 # who to send mail to on error; leave empty to turn off mailing.
359 MAILTO="email@address.org"
360 # number of backup package list created on a Debian systems with "dpkg
361 # --get-selections"; must be a number lower equal to 10. Set to 0 to
362 # turn off backing up of the package list.
363 PACKAGES=3
364 --------------------------------
365
366 Copyright 2005, Sebastian Ley <sebastian.ley@withouthat.org> and
367 Thorsten Bonow <men@withouthat.org>.
368
369 This program is free software; you can redistribute it and/or
370 modify it under the terms of the GNU General Public License as
371 published by the Free Software Foundation; either version 2 of the
372 License, or (at your option) any later version.
373
374 This program is distributed in the hope that it will be useful,
375 but WITHOUT ANY WARRANTY; without even the implied warranty of
376 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
377 General Public License for more details.
378
379 $(usage)
380
381 Options:
382
383 -h, --help Display this help message and exit.
384
385 Arguments:
386
387 none
388
389 NOTE: You must be the superuser to run this script.
390
391 EOF
392
393 }
394
395
396 function root_check() {
397
398
399
400
401
402
403 if [ "$(id | sed 's/uid=\([0-9]*\).*/\1/')" != "0" ]; then
404 error_exit "You must be the superuser to run this script."
405 fi
406
407 }
408
409
410 function check_config() {
411
412
413
414
415
416
417
418 local file
419
420 echo -n "Checking configuration..."
421 for file in "$LOCATIONS_FILE" "$FTP_CONF_FILE" "$CONF_FILE"; do
422 if [ ! -f $file ]; then
423 error_exit "Missing configuration file '$file'."
424 fi
425 if [ "$(stat -c '%U %a' $file 2>/dev/null)" != "root 600" ]; then
426 error_exit "File '$file' isn't owned by root:root with permissions set to 0600."
427 fi
428 if [ "$file" != "$LOCATIONS_FILE" ]; then
429 source $file || error_exit "Couldn't source file '$file'."
430 fi
431 done
432 for variable in "$HOST" "$USER" "$PASSWORD" "$PACKAGES"; do
433 if [ -z "$variable" ]; then
434 error_exit "Variable '$variable' not set correctly."
435 fi
436 done
437 if [[ $PACKAGES -lt 0 || $PACKAGES -gt $MAX_COPIES ]]; then
438 error_exit "PACKAGES option set to invalid value '$PACKAGES'."
439 fi
440 echo "done."
441 return
442
443 }
444
445
446 function create_tarballs() {
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461 local directory
462 local days
463 local name
464 local copies
465 local i=1
466
467
468 while read; do
469 if [ -n "$REPLY" ] && ! echo "$REPLY" | grep -q ^\ *\# ; then
470 directory=$(echo "$REPLY" | awk '{print $1}')
471 days=$(echo "$REPLY" | awk '{print $2}')
472 name=$(echo "$REPLY" | awk '{print $3}')$BACKUP_SUFFIX
473 copies=$(echo "$REPLY" | awk '{print $4}')
474 if [[ $copies -le 0 || $copies -gt $MAX_COPIES ]]; then
475 error_exit "Invalid number of backup copies '$copies' for file '$name'."
476 fi
477 if echo $days | grep -q $(date +%u); then
478 echo -n "Creating tarball for directory '$directory'..."
479 /usr/bin/nice -n 19 tar -c -z -f "$TEMP_DIR1/$name" "$directory" > /dev/null 2>&1
480 if [ $? -ne 0 ]; then
481 error_exit "Error creating tar archieve '$name'."
482 fi
483 if [ -z "$archive_list" ]; then
484 archive_list="$name"
485 else
486
487 echo "$archive_list" | grep $name > /dev/null 2>&1
488 if [ $? -eq 0 ]; then
489 error_exit "Archive '$name' already backed up."
490 else
491 archive_list="$archive_list $name"
492 fi
493 fi
494 number_of_backup_copies_array[$i]="$copies"
495 i=$[ i + 1 ]
496 echo "done."
497 fi
498 fi
499 done < $LOCATIONS_FILE
500 if [ $? -ne 0 ]; then
501 error_exit "Error reading file '$LOCATIONS_FILE'."
502 fi
503 return
504
505 }
506
507
508 function dump_packages_list() {
509
510
511
512
513
514
515
516
517
518
519
520
521 echo -n "Dumping packages list..."
522 dpkg --get-selections | gzip - > ${TEMP_DIR1}/packages$PACKAGE_LIST_SUFFIX
523 if [ $? -ne 0 ]; then
524 error_exit "Error creating packages list '${TEMP_DIR1}/packages$PACKAGE_LIST_SUFFIX'."
525 fi
526 if [ -z "$archive_list" ]; then
527 archive_list="packages$PACKAGE_LIST_SUFFIX"
528 else
529
530 echo "$archive_list" | grep "packages$PACKAGE_LIST_SUFFIX" > /dev/null 2>&1
531 if [ $? -eq 0 ]; then
532 error_exit "Archive 'packages$PACKAGE_LIST_SUFFIX' already backed up."
533 else
534
535 archive_list="packages$PACKAGE_LIST_SUFFIX $archive_list"
536 fi
537 fi
538
539 number_of_backup_copies_array[0]="$PACKAGES"
540 echo "done."
541 return
542
543 }
544
545
546 function ftp_remote_command() {
547
548
549
550
551
552
553
554
555
556 if [ "$1" = "" ]; then
557 error_exit "ftp_remote_command: missing argument 1"
558 fi
559
560 ftp -n $HOST > /dev/null 2>&1 <<End-Of-FTP-Session
561
562 user $USER $PASSWORD
563 binary
564 prompt
565 lcd $TEMP_DIR1
566 $1
567 bye
568 End-Of-FTP-Session
569
570 return $?
571
572 }
573
574
575 function ftp_get_file_list() {
576
577
578
579
580
581
582 echo -n "Getting file list over FTP..."
583 ftp_remote_command "ls . $TEMP_FILE1"
584 if [ $? -ne 0 ]; then
585 error_exit "Error getting FTP file list."
586 fi
587 echo "done."
588 return
589
590 }
591
592
593 function ftp_upload() {
594
595
596
597
598
599
600 local archive
601
602 echo -n "Starting FTP upload..."
603 for archive in ${archive_list}; do
604 grep "$archive" $TEMP_FILE1 >/dev/null 2>&1 && \
605 error_exit "Archive '$archive' already on FTP server."
606 done
607 ftp_remote_command "mput $archive_list"
608 if [ $? -ne 0 ]; then
609 error_exit "Error uploading archives."
610 fi
611 echo "done."
612 return
613
614 }
615
616
617 function ftp_delete() {
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648 local archive
649
650 local file
651
652 local i
653
654 local j=1
655
656
657 ftp_get_file_list
658
659 if [ $PACKAGES -ne 0 ]; then
660
661
662 i=0
663 else
664 i=1
665 fi
666 echo -n "Start deleting old backup archives..."
667 for archive in $archive_list; do
668 if [ "$archive" == "packages$PACKAGE_LIST_SUFFIX" ]; then
669 archive=$(basename $archive $PACKAGE_LIST_SUFFIX)
670 else
671 archive=$(basename $archive $BACKUP_SUFFIX)
672 fi
673
674
675
676
677
678
679 for file in $(cat $TEMP_FILE1 | awk '{print $9}' | grep "$archive.[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(.tar\)\{0,1\}.gz" | sort -r); do
680 if [ $j -gt $((${number_of_backup_copies_array[$i]})) ]; then
681 if [ -z "$delete_list" ]; then
682 delete_list="$file"
683 else
684
685 echo "$delete_list" | grep $file > /dev/null 2>&1
686 if [ $? -eq 0 ]; then
687 error_exit "Archive '$file' already selected for deletion."
688 else
689 delete_list="$delete_list $file"
690 fi
691 fi
692 fi
693 j=$[ j + 1 ]
694 done
695 j=1
696 i=$[ i + 1 ]
697 done
698 ftp_remote_command "mdelete $delete_list"
699 if [ $? -ne 0 ]; then
700 error_exit "Error deleting old archives."
701 fi
702 echo "done."
703 return
704
705 }
706
707
708 function ftp_verify() {
709
710
711
712
713
714
715
716 local archive
717 local error_flag=""
718 local missing_file_list
719 local not_deleted_file_list
720
721
722 ftp_get_file_list
723
724 echo -n "Start verifying backup transactions..."
725
726 for archive in ${archive_list}; do
727 grep "$archive" $TEMP_FILE1 >/dev/null 2>&1
728 if [ $? -ne 0 ]; then
729 echo "Error: Could not find archive '$archive' on FTP server!"
730 error_flag="raised"
731 if [ -z "$missing_file_list" ]; then
732 missing_file_list="$archive"
733 else
734 missing_file_list="$missing_file_list $archive"
735 fi
736 fi
737 done
738
739
740 for archive in ${delete_list}; do
741 grep "$archive" $TEMP_FILE1 >/dev/null 2>&1
742 if [ $? -eq 0 ]; then
743 echo "Error: Archive '$archive' still found on FTP server!"
744 error_flag="raised"
745 if [ -z "$not_deleted_file_list" ]; then
746 not_deleted_file_list="$archive"
747 else
748 not_deleted_file_list="$not_deleted_file_list $archive"
749 fi
750 fi
751 done
752 if [ -n "$error_flag" ]; then
753 error_exit "Archives '$missing_file_list' not found on FTP server, archives '$not_deleted_file_list' still there."
754 fi
755 echo "done."
756 return
757
758 }
759
760
761
762
763
764
765
766
767 root_check
768
769
770
771 trap "signal_exit TERM" TERM HUP
772 trap "signal_exit INT" INT
773
774
775
776 make_temp_files
777
778
779
780 if [ -f "$PIDFILE" ]; then
781 error_exit "PID file '$PIDFILE' already exists."
782 else
783 echo $$ > $PIDFILE || error_exit "Couldn't create PID file '$PIDFILE'."
784 fi
785
786
787
788
789
790
791 if [ "$1" = "--help" ]; then
792 helptext
793 graceful_exit
794 fi
795
796 while getopts :h OPT; do
797 case $OPT in
798 h)
799 helptext
800 graceful_exit
801 ;;
802 *)
803 usage
804 clean_up
805 exit 1
806 esac
807 done
808
809
810
811
812
813
814
815 declare archive_list
816
817
818
819
820
821 declare -a number_of_backup_copies_array
822
823
824 declare delete_list
825
826 echo "Starting new backup on $CURDATE."
827 check_config
828 create_tarballs
829 if [ $PACKAGES -ne 0 ]; then
830 dump_packages_list
831 fi
832 ftp_get_file_list
833 ftp_upload
834 ftp_delete
835 ftp_verify
836 echo "Backup finished. Exiting..."
837
838 graceful_exit
839
840
841
842
843
844
845
846
847