diff --git a/zmbh/README.md b/zmbh/README.md new file mode 100644 index 0000000..a48c410 --- /dev/null +++ b/zmbh/README.md @@ -0,0 +1,22 @@ +# Backup Helper + +This script is an helper for backup operation. It won't backup anything by itself, but will prepare everything so your backup software can do its job. I made it for BackupPC but you can use it with anything else. + +It will try to : + * Shut down Zimbra services to ensure data integrity (optionaly, you can choose to skip this part, or to only shut down LDAP which is the most sensitive service) + * Create a snapshot of the volume where /opt/zimbra is (only LVM supported right now) + * Mount the snapshoted FS on a specified dir, making sure Zimbra's tree appears where you asked + * Restart any service + +Now, you can backup the snapshot with any backup tool, data is frozen. Once the backup is finished, just call the script with the --post arg to cleanup everything + +Arguments : + + * --pre or --post : Set if we run a pre or post backup routine. Default is --pre + * --snap-size : Size of the snapshot to create, for standard LVM volume. Default is 5G. Ignored for LVM thin volumes + * --mount : specify the directory where Zimbra tree should be mounted. Default is /home/lbkp/zimbra/mount + * --quiet : less details will be printed during execution + * --verbose : more details will be printed during execution + * --shutdown=[none|all|ldap] : select which components should be sutted down before taking the snapshot. Default is all, and will stop all Zimbra services. You can choose none (no Zimbra service will be shutted down) or ldap (only ldap, if installed, will be shutted down) + + diff --git a/zmbh/zmbh.pl b/zmbh/zmbh.pl new file mode 100644 index 0000000..38e9246 --- /dev/null +++ b/zmbh/zmbh.pl @@ -0,0 +1,185 @@ +#!/usr/bin/perl -w + +use warnings; +use strict; +use Getopt::Long; +use File::Path; +use File::Which; +use JSON; + +my $opt = { + shutdown => 'all', + snap_size => '5G', + snapshot => 1, + mount => '/home/lbkp/zimbra/mount', + pre => 1, + post => 0, + quiet => 0, + verbose => 0 +}; + +GetOptions ( + 'shutdown=s' => \$opt->{shutdown}, + 'snap-size=s' => \$opt->{snap_size}, + 'snapshot!' => \$opt->{snapshot}, + 'mount=s' => \$opt->{mount}, + 'pre' => \$opt->{pre}, + 'post' => \$opt->{post}, + 'quiet' => \$opt->{quiet}, + 'verbose' => \$opt->{verbose} +); + +if ( not -d $opt->{mount} ) { + die $opt->{mount} . " must exist\n"; +} + +$opt->{pre} = 0 if $opt->{post}; + +# Start by assuming we can run snapshots +my $can_snapshot = 1; + +my $lvs = which('lvs'); + +if (not $lvs) { + log_info("lvs not found, no snapshot will be attempted"); + $can_snapshot = 0; +} + +my $lv_info = {}; + +my ($dev, $fs, undef, undef, undef, undef, $mp) = split /\s+/, ( qx( df -PTl /opt/zimbra ) )[1]; +log_verbose("Found device $dev mounted on $mp with an $fs filesystem"); + +if ( $can_snapshot ) { + log_verbose("Trying to detect if $dev is an LVM volume"); + $lv_info = from_json(qx( $lvs --reportformat=json -o vg_name,lv_name,pool_lv $dev 2>/dev/null)); + if (defined $lv_info->{report}->[0]->{lv}->[0] ){ + $lv_info = $lv_info->{report}->[0]->{lv}->[0]; + } +} + +if ( $opt->{pre} ) { + if ($opt->{shutdown} =~ m/^no(ne)?/){ + log_info("Not shutting down any service"); + } elsif ($opt->{shutdown} eq 'ldap' and -e '/opt/zimbra/bin/ldap'){ + log_info("Stoping Zimbra LDAP service"); + system("/opt/zimbra/bin/ldap stop"); + } else { + log_info("Stoping Zimbra services"); + system('systemctl stop zimbra'); + } + if ( not $lv_info->{vg_name} or not $lv_info->{lv_name} or $lv_info->{vg_name} eq '' or $lv_info->{lv_name} eq '' ) { + # We cannot take a snapshot. Zimbra will just be kept shut down until the end of the backup (unless you choose not to shut down services) + # Just bind mount /opt/zimbra on the backup dir + log_info("Can't create a snapshot of $dev"); + if ( system('mount -o bind,ro /opt/zimbra ' . $opt->{mount} ) ) { + die "Can't mount /opt/zimbra on $opt->{mount}\n"; + } + } else { + log_info("Trying to create a snapshot of device $dev"); + my $snap_args = '-s -n ' . $lv_info->{lv_name} . '_bkp'; + # Detect if thin pool or standard LVM + if ( defined $lv_info->{pool_lv} and $lv_info->{pool_lv} ne '' ) { + # Thin LVM + log_verbose("$dev is a thin LVM volume"); + $snap_args .= ' -kn'; + } else { + # Standard LVM + log_verbose("$dev is a standard LVM volume"); + $snap_args .= ' -L' . $opt->{snap_size}; + } + + # Take the snapshot + if ( system( "lvcreate $snap_args $dev") != 0 ) { + die "Failed to create snapshot\n"; + } + + log_info("snapshot created as $dev" . '_bkp'); + + # Restart Zimbra now to minimize down time + if ($opt->{shutdown} =~ m/^no(ne)?/){ + log_info("No service were shutted down"); + } elsif ($opt->{shutdown} eq 'ldap' and -e '/opt/zimbra/bin/ldap'){ + log_info("Starting Zimbra LDAP service"); + system("/opt/zimbra/bin/ldap start"); + } else { + log_info("Starting Zimbra services"); + system('systemctl start zimbra'); + } + + # Now mount the snapshot RO + my $mount_args = "-o ro -t $fs"; + if ( $fs eq 'xfs' ) { + $mount_args .= ' -o nouuid'; + } + + + log_verbose("Mounting the snapshot readonly on $opt->{mount}"); + if ( system("mount $mount_args /dev/mapper/" . $lv_info->{vg_name} . '-' . $lv_info->{lv_name} . '_bkp ' . $opt->{mount}) != 0 ) { + die "Can't mount " . $lv_info->{lv_name} . '_bkp on ' . $opt->{mount} . "\n"; + } + + # The snapshot is mounted, but we might need an additional bind mount if the volume hosts / or /opt + my $level = grep { $_ ne '' } split( /\//, $mp); + my $level2subdir = { + 0 => '/opt/zimbra', + 1 => '/zimbra' + }; + if ( defined $level2subdir->{$level} ) { + if ( system('mount -o bind,ro ' . $opt->{mount} . $level2subdir->{$level} . ' ' . $opt->{mount} ) ) { + die "Can't mount $opt->{mount}$level2subdir->{$level} on $opt->{mount}\n"; + } + } + } +} elsif ( $opt->{post} ) { + log_info("unmounting $opt->{mount}"); + while (is_mounted($opt->{mount})){ + # We need to loop as we can have a stacked bind mount over the standard FS + system( "umount $opt->{mount}" ); + } + + if ( not $lv_info->{vg_name} or not $lv_info->{lv_name} or $lv_info->{vg_name} eq '' or $lv_info->{lv_name} eq '' ) { + # No backup snapshot, zimbra should just be started again + log_info("Restating Zimbra services"); + system('systemctl start zimbra'); + } else { + log_verbose("Removing LVM snapshot"); + if ( system( "lvremove -y $lv_info->{vg_name}/$lv_info->{lv_name}" . '_bkp' ) != 0 ) { + die "Failed to remove LVM snapshot\n"; + } + } +} +# Print messages only if the verbose flag was given +sub log_verbose { + my $msg = shift; + print $msg . "\n" if ( $opt->{verbose} ); +} + +# Print info messages unless the quiet flag was given +sub log_info { + my $msg = shift; + print $msg . "\n" if ( not $opt->{quiet} ); +} + +# Print errors +sub log_error { + my $msg = shift; + print $msg . "\n"; +} + +# Check if something is mounted on a dir +sub is_mounted { + my $dir = shift; + $dir =~ s/\/$//; + my $is_mounted = 0; + open MOUNTS, '){ + my ($what, $where, $type, $options) = split(/\s+/, $_); + if ($where eq $dir){ + $is_mounted = 1; + last; + } + } + close MOUNTS; + return $is_mounted; +}