@ -26,6 +26,7 @@
use XML::Simple;
use XML::Simple;
use Sys::Virt;
use Sys::Virt;
use Getopt::Long;
use Getopt::Long;
use File::Copy;
# Set umask
# Set umask
umask(022);
umask(022);
@ -54,13 +55,15 @@ $opts{keeplock} = 0;
# Should we try to create LVM snapshots during the dump ?
# Should we try to create LVM snapshots during the dump ?
$opts{snapshot} = 1;
$opts{snapshot} = 1;
# Libvirt URI to connect to
# Libvirt URI to connect to
$opts{connect} = "qemu:///system" ;
@connect = () ;
# Compression used with the dump action (the compression is done on the fly)
# Compression used with the dump action (the compression is done on the fly)
$opts{compress} = 'none';
$opts{compress} = 'none';
# lvcreate path
# lvcreate path
$opts{lvcreate} = '/sbin/lvcreate -c 512';
$opts{lvcreate} = '/sbin/lvcreate -c 512';
# lvremove path
# lvremove path
$opts{lvremove} = '/sbin/lvremove';
$opts{lvremove} = '/sbin/lvremove';
# Override path to the LVM backend
$opts{lvm} = '';
# chunkfs path
# chunkfs path
$opts{chunkfs} = '/usr/bin/chunkfs';
$opts{chunkfs} = '/usr/bin/chunkfs';
# Size of chunks to use with chunkfs or or blocks with dd in bytes (default to 256kB)
# Size of chunks to use with chunkfs or or blocks with dd in bytes (default to 256kB)
@ -84,12 +87,13 @@ GetOptions(
"state" => \$opts{state},
"state" => \$opts{state},
"snapsize=s" => \$opts{snapsize},
"snapsize=s" => \$opts{snapsize},
"backupdir=s" => \$opts{backupdir},
"backupdir=s" => \$opts{backupdir},
"lvm=s" => \$opts{lvm},
"vm=s" => \@vms,
"vm=s" => \@vms,
"action=s" => \$opts{action},
"action=s" => \$opts{action},
"cleanup" => \$opts{cleanup},
"cleanup" => \$opts{cleanup},
"dump" => \$opts{dump},
"dump" => \$opts{dump},
"unlock" => \$opts{unlock},
"unlock" => \$opts{unlock},
"connect=s" => \$opts{connect} ,
"connect=s" => \@connect ,
"snapshot!" => \$opts{snapshot},
"snapshot!" => \$opts{snapshot},
"compress:s" => \$opts{compress},
"compress:s" => \$opts{compress},
"exclude=s" => \@excludes,
"exclude=s" => \@excludes,
@ -138,6 +142,10 @@ else{
# Allow comma separated multi-argument
# Allow comma separated multi-argument
@vms = split(/,/,join(',',@vms));
@vms = split(/,/,join(',',@vms));
@excludes = split(/,/,join(',',@excludes));
@excludes = split(/,/,join(',',@excludes));
@connect = split(/,/,join(',',@connect));
# Define a default libvirt URI
$connect[0] = "qemu:///system" unless (defined $connect[0]);
# Backward compatible with --dump --cleanup --unlock
# Backward compatible with --dump --cleanup --unlock
$opts{action} = 'dump' if ($opts{dump});
$opts{action} = 'dump' if ($opts{dump});
@ -163,18 +171,40 @@ if (! -d $opts{backupdir} ){
}
}
# Connect to libvirt
# Connect to libvirt
print "\n\nConnecting to libvirt daemon using $opts{connect} as URI\n" if ($opts{debug});
print "\n\nConnecting to libvirt daemon using $connect[0] as URI\n" if ($opts{debug});
our $libvirt = Sys::Virt->new( uri => $opts{connect} ) ||
$libvirt1 = Sys::Virt->new( uri => $connect[0] ) ||
die "Error connecting to libvirt on URI: $opts{connect}";
die "Error connecting to libvirt on URI: $connect[0]";
if (defined $connect[1]){
$libvirt2 = Sys::Virt->new( uri => $connect[1] ) ||
die "Error connecting to libvirt on URI: $connect[1]";
}
our $libvirt = $libvirt1;
print "\n" if ($opts{debug});
print "\n" if ($opts{debug});
foreach our $vm (@vms){
foreach our $vm (@vms){
# Create a new object representing the VM
# Create a new object representing the VM
print "Checking $vm status\n\n" if ($opts{debug});
print "Checking $vm status\n\n" if ($opts{debug});
our $dom = $libvirt->get_domain_by_name($vm) ||
our $dom = $libvirt1 ->get_domain_by_name($vm) ||
die "Error opening $vm object";
die "Error opening $vm object";
# If we've passed two connect URI, and our VM is not
# running on the first one, check on the second one
if (!$dom->is_active && defined $connect[1]){
$dom = $libvirt2->get_domain_by_name($vm) ||
die "Error opening $vm object";
if ($dom->is_active()){
$libvirt = $libvirt2;
}
else{
$dom = $libvirt1->get_domain_by_name($vm);
}
}
our $backupdir = $opts{backupdir}.'/'.$vm;
our $backupdir = $opts{backupdir}.'/'.$vm;
our $time = "_".time();
if ($opts{action} eq 'cleanup'){
if ($opts{action} eq 'cleanup'){
print "Running cleanup routine for $vm\n\n" if ($opts{debug});
print "Running cleanup routine for $vm\n\n" if ($opts{debug});
run_cleanup();
run_cleanup();
@ -187,12 +217,14 @@ foreach our $vm (@vms){
print "Running dump routine for $vm\n\n" if ($opts{debug});
print "Running dump routine for $vm\n\n" if ($opts{debug});
mkdir $backupdir || die $!;
mkdir $backupdir || die $!;
mkdir $backupdir . '.meta' || die $!;
mkdir $backupdir . '.meta' || die $!;
mkdir $backupdir . '.mount' || die $!;
run_dump();
run_dump();
}
}
elsif ($opts{action} eq 'chunkmount'){
elsif ($opts{action} eq 'chunkmount'){
print "Running chunkmount routine for $vm\n\n" if ($opts{debug});
print "Running chunkmount routine for $vm\n\n" if ($opts{debug});
mkdir $backupdir || die $!;
mkdir $backupdir || die $!;
mkdir $backupdir . '.meta' || die $!;
mkdir $backupdir . '.meta' || die $!;
mkdir $backupdir . '.mount' || die $!;
run_chunkmount();
run_chunkmount();
}
}
else {
else {
@ -270,7 +302,6 @@ sub prepare_backup{
# If it's a block device
# If it's a block device
if ($disk->{type} eq 'block'){
if ($disk->{type} eq 'block'){
my $time = "_".time();
# Try to snapshot the source if snapshot is enabled
# Try to snapshot the source if snapshot is enabled
if ( ($opts{snapshot}) && (create_snapshot($source,$time)) ){
if ( ($opts{snapshot}) && (create_snapshot($source,$time)) ){
print "$source seems to be a valid logical volume (LVM), a snapshot has been taken as " .
print "$source seems to be a valid logical volume (LVM), a snapshot has been taken as " .
@ -292,8 +323,42 @@ sub prepare_backup{
}
}
}
}
elsif ($disk->{type} eq 'file'){
elsif ($disk->{type} eq 'file'){
$opts{livebackup} = 0;
# Try to find the mount point, and the backing device
push (@disks, {source => $source, target => $target, type => 'file'});
my @df = `df -P $source`;
my ($dev,undef,undef,undef,undef,$mount) = split /\s+/, $df[1];
# The backing device can be detected, but can also be overwritten with --lvm=/dev/data/vm
# This can be usefull for example when you use GlusterFS. Df will return something like
# localhost:/vmstore as the device, but this GlusterFS volume might be backed by an LVM
# volume, in which case, you can pass it as an argument to the script
my $lvm = ($opts{lvm} ne '' && -e "$opts{lvm}") ? $opts{lvm} : $dev;
# Try to snapshot this device
if ( $opts{snapshot} ){
# Maybe the LVM is already snapshoted and mounted for a previous disk ?
my $is_mounted = 0;
if (open MOUNT, "<$backupdir.meta/mount"){
while (<MOUNT>){
$is_mounted = 1 if ($_ eq $lvm);
}
close MOUNT;
}
if (!$is_mounted && create_snapshot($lvm,$time)){
print "$lvm seems to be a valid logical volume (LVM), a snapshot has been taken as " .
$lvm . $time ."\n" if ($opts{debug});
my $snap = $lvm.$time;
# -o nouuid is needed if XFS is used
system("/bin/mount -o nouuid $snap $backupdir.mount");
open MOUNT, ">$backupdir.meta/mount";
print MOUNT $lvm;
close MOUNT;
}
my $file = $source;
$file =~ s|$mount|$backupdir.mount|;
push (@disks, {source => $file, target => $target, type => 'file'});
}
else {
$opts{livebackup} = 0;
push (@disks, {source => $source, target => $target, type => 'file'});
}
}
}
print "Adding $source to the list of disks to be backed up\n" if ($opts{debug});
print "Adding $source to the list of disks to be backed up\n" if ($opts{debug});
}
}
@ -405,12 +470,26 @@ sub run_cleanup{
}
}
if (open MOUNTS, "</proc/mounts"){
if (open MOUNTS, "</proc/mounts"){
foreach (<MOUNTS>){
my @mounts = <MOUNTS>;
# We first need to umount chunkfs mount points
foreach (@mounts){
my @info = split(/\s+/, $_);
my @info = split(/\s+/, $_);
next unless ($info[0] eq "chunkfs-$vm");
next unless ($info[0] eq "chunkfs-$vm");
print "Found chunkfs mount point: $info[1]\n" if ($opts{debug});
print "Found chunkfs mount point: $info[1]\n" if ($opts{debug});
my $mp = $info[1];
my $mp = $info[1];
print "Unmounting chunkfs mount point $mp\n\n" if ($opts{debug});
print "Unmounting $mp\n\n" if ($opts{debug});
die "Couldn't unmount $mp\n" unless (
system("/bin/umount $mp 2>/dev/null") == 0
);
rmdir $mp || die $!;
}
# Now, standard filesystems
foreach (@mounts){
my @info = split(/\s+/, $_);
next unless ($info[1] eq "$backupdir.mount");
print "Found temporary mount point: $info[1]\n" if ($opts{debug});
my $mp = $info[1];
print "Unmounting $mp\n\n" if ($opts{debug});
die "Couldn't unmount $mp\n" unless (
die "Couldn't unmount $mp\n" unless (
system("/bin/umount $mp 2>/dev/null") == 0
system("/bin/umount $mp 2>/dev/null") == 0
);
);
@ -441,6 +520,7 @@ sub run_cleanup{
$meta = unlink <$backupdir.meta/*>;
$meta = unlink <$backupdir.meta/*>;
rmdir "$backupdir/";
rmdir "$backupdir/";
rmdir "$backupdir.meta";
rmdir "$backupdir.meta";
rmdir "$backupdir.mount";
print "$cnt file(s) removed\n$snap LVM snapshots removed\n$meta metadata files removed\n\n" if $opts{debug};
print "$cnt file(s) removed\n$snap LVM snapshots removed\n$meta metadata files removed\n\n" if $opts{debug};
}
}
@ -495,7 +575,20 @@ sub usage{
sub save_vm_state{
sub save_vm_state{
if ($dom->is_active()){
if ($dom->is_active()){
print "$vm is running, saving state....\n" if ($opts{debug});
print "$vm is running, saving state....\n" if ($opts{debug});
$dom->save("$backupdir/$vm.state");
# if connect[1] is defined, you've passed several connection URI
# This means that you're running a dual hypervisor cluster
# And depending on the one running the current VM
# $backupdir might not be available
# whereas /var/lib/libvirt/qemu/save/ might
# if you've mounted here a shared file system
# (NFS, GlusterFS, GFS2, OCFS etc...)
if (defined $connect[1]){
$dom->managed_save();
move "/var/lib/libvirt/qemu/save/$vm.save", "$backupdir/$vm.state";
}
else{
$dom->save("$backupdir/$vm.state");
}
print "$vm state saved as $backupdir/$vm.state\n" if ($opts{debug});
print "$vm state saved as $backupdir/$vm.state\n" if ($opts{debug});
}
}
else{
else{
@ -507,16 +600,29 @@ sub save_vm_state{
sub restore_vm{
sub restore_vm{
if (! $dom->is_active()){
if (! $dom->is_active()){
if (-e "$backupdir/$vm.state"){
if (-e "$backupdir/$vm.state"){
print "\nTrying to restore $vm from $backupdir/$vm.state\n" if ($opts{debug});
# if connect[1] is defined, you've passed several connection URI
$libvirt->restore_domain("$backupdir/$vm.state");
# This means that you're running a dual hypervisor cluster
print "Waiting for restoration to complete\n" if ($opts{debug});
# And depending on the one running the current VM
my $i = 0;
# $backupdir might not be available
while ((!$dom->is_active()) && ($i < 120)){
# whereas /var/lib/libvirt/qemu/save/ might
sleep(5);
# if you've mounted here a shared file system
$i = $i+5;
# (NFS, GlusterFS, GFS2, OCFS etc...)
if (defined $connect[1]){
copy "$backupdir/$vm.state", "/var/lib/libvirt/qemu/save/$vm.save";
start_vm();
}
else{
print "\nTrying to restore $vm from $backupdir/$vm.state\n" if ($opts{debug});
$libvirt->restore_domain("$backupdir/$vm.state");
print "Waiting for restoration to complete\n" if ($opts{debug});
my $i = 0;
while ((!$dom->is_active()) && ($i < 120)){
sleep(5);
$i = $i+5;
}
print "Timeout while trying to restore $vm, aborting\n"
if (($i > 120) && ($opts{debug}));
}
}
print "Timeout while trying to restore $vm, aborting\n"
if (($i > 120) && ($opts{debug}));
}
}
else{
else{
print "\nRestoration impossible, $backupdir/$vm.state is missing\n" if ($opts{debug});
print "\nRestoration impossible, $backupdir/$vm.state is missing\n" if ($opts{debug});
@ -597,9 +703,9 @@ sub save_xml{
sub create_snapshot{
sub create_snapshot{
my ($blk,$suffix) = @_;
my ($blk,$suffix) = @_;
my $ret = 0;
my $ret = 0;
print "Running: $opts{lvcreate} -p r - s -n " . $blk . $suffix .
print "Running: $opts{lvcreate} -s -n " . $blk . $suffix .
" -L $opts{snapsize} $blk > /dev/null 2>&1 < /dev/null\n" if $opts{debug};
" -L $opts{snapsize} $blk > /dev/null 2>&1 < /dev/null\n" if $opts{debug};
if ( system("$opts{lvcreate} -p r - s -n " . $blk . $suffix .
if ( system("$opts{lvcreate} -s -n " . $blk . $suffix .
" -L $opts{snapsize} $blk > /dev/null 2>&1 < /dev/null") == 0 ) {
" -L $opts{snapsize} $blk > /dev/null 2>&1 < /dev/null") == 0 ) {
$ret = 1;
$ret = 1;
open SNAPLIST, ">>$backupdir.meta/snapshots" or die "Error, couldn't open snapshot list file\n";
open SNAPLIST, ">>$backupdir.meta/snapshots" or die "Error, couldn't open snapshot list file\n";