#!/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} ) { my $failure = 0; 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 ) { log_info("Failed to create snapshot"); # Record the failure but don't die now, we need to restart services $failure = 1; } else { 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"); my $try = 0; my $running = 0; while ($try < 20) { system("/opt/zimbra/bin/ldap start"); sleep 1; if (system("/opt/zimbra/bin/ldap status") == 0){ $running = 1; last; } else { log_info('ldap service not running, trying again to start it'); $try++; } } # Couldn't start ldap ? Restart all the services if (not $running){ log_info("Failed to restart ldap, restarting all Zimbra services"); system('systemctl restart zimbra'); } } else { log_info("Starting Zimbra services"); system('systemctl start zimbra'); } if ($failure){ die "Stoping backup process now, as snapshot failed\n"; } # 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; }