From ef0c33ae9724922bb7d838f901596bddfd776de8 Mon Sep 17 00:00:00 2001 From: Daniel Berteaud Date: Wed, 24 Jul 2013 12:35:00 +0200 Subject: [PATCH] Support a two nodes cluster situation, where images are files on the host, backed by an LVM volume (and also support the case where GlusterFS is between the LVM and the guest images) --- virt-backup | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 130 insertions(+), 24 deletions(-) diff --git a/virt-backup b/virt-backup index 044c675..606f294 100644 --- a/virt-backup +++ b/virt-backup @@ -26,6 +26,7 @@ use XML::Simple; use Sys::Virt; use Getopt::Long; +use File::Copy; # Set umask umask(022); @@ -54,13 +55,15 @@ $opts{keeplock} = 0; # Should we try to create LVM snapshots during the dump ? $opts{snapshot} = 1; # Libvirt URI to connect to -$opts{connect} = "qemu:///system"; +@connect = (); # Compression used with the dump action (the compression is done on the fly) $opts{compress} = 'none'; # lvcreate path $opts{lvcreate} = '/sbin/lvcreate -c 512'; # lvremove path $opts{lvremove} = '/sbin/lvremove'; +# Override path to the LVM backend +$opts{lvm} = ''; # chunkfs path $opts{chunkfs} = '/usr/bin/chunkfs'; # 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}, "snapsize=s" => \$opts{snapsize}, "backupdir=s" => \$opts{backupdir}, + "lvm=s" => \$opts{lvm}, "vm=s" => \@vms, "action=s" => \$opts{action}, "cleanup" => \$opts{cleanup}, "dump" => \$opts{dump}, "unlock" => \$opts{unlock}, - "connect=s" => \$opts{connect}, + "connect=s" => \@connect, "snapshot!" => \$opts{snapshot}, "compress:s" => \$opts{compress}, "exclude=s" => \@excludes, @@ -138,6 +142,10 @@ else{ # Allow comma separated multi-argument @vms = split(/,/,join(',',@vms)); @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 $opts{action} = 'dump' if ($opts{dump}); @@ -163,18 +171,40 @@ if (! -d $opts{backupdir} ){ } # Connect to libvirt -print "\n\nConnecting to libvirt daemon using $opts{connect} as URI\n" if ($opts{debug}); -our $libvirt = Sys::Virt->new( uri => $opts{connect} ) || - die "Error connecting to libvirt on URI: $opts{connect}"; - +print "\n\nConnecting to libvirt daemon using $connect[0] as URI\n" if ($opts{debug}); +$libvirt1 = Sys::Virt->new( uri => $connect[0] ) || + 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}); foreach our $vm (@vms){ # Create a new object representing the VM 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"; + + # 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 $time = "_".time(); if ($opts{action} eq 'cleanup'){ print "Running cleanup routine for $vm\n\n" if ($opts{debug}); run_cleanup(); @@ -187,12 +217,14 @@ foreach our $vm (@vms){ print "Running dump routine for $vm\n\n" if ($opts{debug}); mkdir $backupdir || die $!; mkdir $backupdir . '.meta' || die $!; + mkdir $backupdir . '.mount' || die $!; run_dump(); } elsif ($opts{action} eq 'chunkmount'){ print "Running chunkmount routine for $vm\n\n" if ($opts{debug}); mkdir $backupdir || die $!; mkdir $backupdir . '.meta' || die $!; + mkdir $backupdir . '.mount' || die $!; run_chunkmount(); } else { @@ -270,7 +302,6 @@ sub prepare_backup{ # If it's a block device if ($disk->{type} eq 'block'){ - my $time = "_".time(); # Try to snapshot the source if snapshot is enabled if ( ($opts{snapshot}) && (create_snapshot($source,$time)) ){ 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'){ - $opts{livebackup} = 0; - push (@disks, {source => $source, target => $target, type => 'file'}); + # Try to find the mount point, and the backing device + 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 (){ + $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}); } @@ -405,12 +470,26 @@ sub run_cleanup{ } if (open MOUNTS, "){ + my @mounts = ; + # We first need to umount chunkfs mount points + foreach (@mounts){ my @info = split(/\s+/, $_); next unless ($info[0] eq "chunkfs-$vm"); print "Found chunkfs mount point: $info[1]\n" if ($opts{debug}); 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 ( system("/bin/umount $mp 2>/dev/null") == 0 ); @@ -441,6 +520,7 @@ sub run_cleanup{ $meta = unlink <$backupdir.meta/*>; rmdir "$backupdir/"; 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}; } @@ -495,7 +575,20 @@ sub usage{ sub save_vm_state{ if ($dom->is_active()){ 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}); } else{ @@ -507,16 +600,29 @@ sub save_vm_state{ sub restore_vm{ if (! $dom->is_active()){ if (-e "$backupdir/$vm.state"){ - 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; + # 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]){ + 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{ print "\nRestoration impossible, $backupdir/$vm.state is missing\n" if ($opts{debug}); @@ -597,9 +703,9 @@ sub save_xml{ sub create_snapshot{ my ($blk,$suffix) = @_; 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}; - 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 ) { $ret = 1; open SNAPLIST, ">>$backupdir.meta/snapshots" or die "Error, couldn't open snapshot list file\n";