Backup script helper for libvirt based VM
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

638 lines
23 KiB

#!/usr/bin/perl -w
# AUTHOR
# Daniel Berteaud <daniel@firewall-services.com>
#
# COPYRIGHT
# Copyright (C) 2009-2012 Daniel Berteaud
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# See README for documentation and examples
use XML::Simple;
use Sys::Virt;
use Getopt::Long;
# Set umask
umask(022);
# Some constant
our %opts = ();
our @vms = ();
our @excludes = ();
our @disks = ();
# Sets some defaults values
# What to run. The default action is to dump
$opts{action} = 'dump';
# Where backups will be stored. This directory must already exists
$opts{backupdir} = '/var/lib/libvirt/backup';
# Size of LVM snapshots (which will be used to backup VM with minimum downtown
# if the VM store data directly on a LV)
$opts{snapsize} = '5G';
# If we should also dump the VM state (dump the mémory, equivalent of virsh save)
$opts{state} = 0;
# Debug
$opts{debug} = 0;
# Let the lock file present after the dump is finisehd
$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";
# 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';
# chunkfs path
$opts{chunkfs} = '/usr/bin/chunkfs';
# Size of chunks to use with chunkfs or or blocks with dd in bytes (default to 256kB)
$opts{blocksize} = '262144';
# nice may be used to reduce CPU priority of compression processes
$opts{nice} = 'nice -n 19';
# ionice may be used to reduce disk access priority of dump/chunkfs processes
# which can be quite I/O intensive. This only works if your storage
# uses the CFQ scheduler (which is the default on EL)
$opts{ionice} = 'ionice -c 2 -n 7';
$opts{shutdown} = 0;
$opts{shutdown_time} = 300;
$opts{livebackup} = 1;
$opts{wasrunning} = 1;
# get command line arguments
GetOptions(
"debug" => \$opts{debug},
"keep-lock" => \$opts{keeplock},
"state" => \$opts{state},
"snapsize=s" => \$opts{snapsize},
"backupdir=s" => \$opts{backupdir},
"vm=s" => \@vms,
"action=s" => \$opts{action},
"cleanup" => \$opts{cleanup},
"dump" => \$opts{dump},
"unlock" => \$opts{unlock},
"connect=s" => \$opts{connect},
"snapshot!" => \$opts{snapshot},
"compress:s" => \$opts{compress},
"exclude=s" => \@excludes,
"blocksize=s" => \$opts{blocksize},
"shutdown" => \$opts{shutdown},
"shutdown-timeout=s" => \$opts{shutdowntimeout},
"help" => \$opts{help}
);
# Set compression settings
if ($opts{compress} eq 'lzop'){
$opts{compext} = ".lzo";
$opts{compcmd} = "lzop -c";
}
elsif ($opts{compress} eq 'bzip2'){
$opts{compext} = ".bz2";
$opts{compcmd} = "bzip2 -c";
}
elsif ($opts{compress} eq 'pbzip2'){
$opts{compext} = ".bz2";
$opts{compcmd} = "pbzip2 -c";
}
elsif ($opts{compress} eq 'xz'){
$opts{compext} = ".xz";
$opts{compcmd} = "xz -c";
}
elsif ($opts{compress} eq 'lzip'){
$opts{compext} = ".lz";
$opts{compcmd} = "lzip -c";
}
elsif ($opts{compress} eq 'plzip'){
$opts{compext} = ".lz";
$opts{compcmd} = "plzip -c";
}
# Default is gzip
elsif (($opts{compress} eq 'gzip') || ($opts{compress} eq '')) {
$opts{compext} = ".gz";
$opts{compcmd} = "gzip -c";
}
else{
$opts{compext} = "";
$opts{compcmd} = "cat";
}
# Allow comma separated multi-argument
@vms = split(/,/,join(',',@vms));
@excludes = split(/,/,join(',',@excludes));
# Backward compatible with --dump --cleanup --unlock
$opts{action} = 'dump' if ($opts{dump});
$opts{action} = 'cleanup' if ($opts{cleanup});
$opts{action} = 'unlock' if ($opts{unlock});
# Stop here if we have no vm
# Or the help flag is present
if ((!@vms) || ($opts{help})){
usage();
exit 1;
}
# Or state and shutdown flags are used together
if (($opts{state}) && ($opts{shutdown})){
print "State and shutdown flags cannot be used together\n";
exit 1;
}
# Backup dir needs to be created first
if (! -d $opts{backupdir} ){
print "$opts{backupdir} is not a valid directory\n";
exit 1;
}
# 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" 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) ||
die "Error opening $vm object";
our $backupdir = $opts{backupdir}.'/'.$vm;
if ($opts{action} eq 'cleanup'){
print "Running cleanup routine for $vm\n\n" if ($opts{debug});
run_cleanup();
}
elsif ($opts{action} eq 'unlock'){
print "Unlocking $vm\n\n" if ($opts{debug});
unlock_vm();
}
elsif ($opts{action} eq 'dump'){
print "Running dump routine for $vm\n\n" if ($opts{debug});
mkdir $backupdir || die $!;
mkdir $backupdir . '.meta' || 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 $!;
run_chunkmount();
}
else {
usage();
exit 1;
}
}
############################################################################
############## FUNCTIONS ####################
############################################################################
# Common routine before backup. Will save the XML description, try to
# create a snapshot of the disks etc...
sub prepare_backup{
# Create a new XML object
my $xml = new XML::Simple ();
my $data = $xml->XMLin( $dom->get_xml_description(), forcearray => ['disk'] );
# Stop here if the lock file is present, another dump might be running
die "Another backup is running\n" if ( -e "$backupdir.meta/$vm.lock" );
# Lock VM: Create a lock file so only one dump process can run
lock_vm();
# Save the XML description
save_xml();
# Save the VM state if it's running and --state is present
# (else, just suspend the VM)
$opts{wasrunning} = 0 unless ($dom->is_active());
if ($opts{wasrunning}){
if ($opts{state}){
save_vm_state();
}
elsif ($opts{shutdown}){
shutdown_vm();
}
else{
suspend_vm();
}
}
# Create a list of disks used by the VM
foreach $disk (@{$data->{devices}->{disk}}){
my $source;
if ($disk->{type} eq 'block'){
$source = $disk->{source}->{dev};
}
elsif ($disk->{type} eq 'file'){
$source = $disk->{source}->{file};
}
else{
print "\nSkiping $source for vm $vm as it's type is $disk->{type}: " .
" and only block and file are supported\n" if ($opts{debug});
next;
}
my $target = $disk->{target}->{dev};
# Check if the current disk is not excluded
if (grep { $_ eq "$target" } @excludes){
print "\nSkiping $source for vm $vm as it's matching one of the excludes: " .
join(",",@excludes)."\n\n" if ($opts{debug});
next;
}
# If the device is a disk (and not a cdrom) and the source dev exists
if (($disk->{device} eq 'disk') && (-e $source)){
print "\nAnalysing disk $source connected on $vm as $target\n\n" if ($opts{debug});
# 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 " .
$source . $time ."\n" if ($opts{debug});
$source = $source.$time;
push (@disks, {source => $source, target => $target, type => 'snapshot'});
}
# Snapshot failed, or disabled: disabling live backups
else{
if ($opts{snapshot}){
print "Snapshoting $source has failed (not managed by LVM, or already a snapshot ?)" .
", live backup will be disabled\n" if ($opts{debug}) ;
}
else{
print "Not using LVM snapshots, live backups will be disabled\n" if ($opts{debug});
}
$opts{livebackup} = 0;
push (@disks, {source => $source, target => $target, type => 'block'});
}
}
elsif ($disk->{type} eq 'file'){
$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});
}
}
# Summarize the list of disk to be dumped
if ($opts{debug}){
if ($opts{action} eq 'dump'){
print "\n\nThe following disks will be dumped:\n\n";
foreach $disk (@disks){
print "Source: $disk->{source}\tDest: $backupdir/$vm" . '_' . $disk->{target} .
".img$opts{compext}\n";
}
}
elsif($opts{action} eq 'chunkmount'){
print "\n\nThe following disks will be mounted as chunks:\n\n";
foreach $disk (@disks){
print "Source: $disk->{source}\tDest: $backupdir/$vm" . '_' . $disk->{target};
}
}
}
# If livebackup is possible (every block devices can be snapshoted)
# We can restore the VM now, in order to minimize the downtime
if ($opts{livebackup}){
print "\nWe can run a live backup\n" if ($opts{debug});
if ($opts{wasrunning}){
if ($opts{state}){
# Prevent a race condition in libvirt
sleep(1);
restore_vm();
}
elsif ($opts{shutdown}){
start_vm();
}
else{
resume_vm();
}
}
}
}
sub run_dump{
# Pause VM, dump state, take snapshots etc..
prepare_backup();
# Now, it's time to actually dump the disks
foreach $disk (@disks){
my $source = $disk->{source};
my $dest = "$backupdir/$vm" . '_' . $disk->{target} . ".img$opts{compext}";
print "\nStarting dump of $source to $dest\n\n" if ($opts{debug});
my $ddcmd = "$opts{ionice} dd if=$source bs=$opts{blocksize} | $opts{nice} $opts{compcmd} > $dest 2>/dev/null";
unless( system("$ddcmd") == 0 ){
die "Couldn't dump the block device/file $source to $dest\n";
}
# Remove the snapshot if the current dumped disk is a snapshot
sleep(1);
destroy_snapshot($source) if ($disk->{type} eq 'snapshot');
}
# If the VM was running before the dump, restore (or resume) it
if ($opts{wasrunning}){
if ($opts{state}){
restore_vm();
}
else{
resume_vm();
}
}
# And remove the lock file, unless the --keep-lock flag is present
unlock_vm() unless ($opts{keeplock});
}
sub run_chunkmount{
# Pause VM, dump state, take snapshots etc..
prepare_backup();
# Now, lets mount guest images with chunkfs
foreach $disk (@disks){
my $source = $disk->{source};
my $dest = "$backupdir/$vm" . '_' . $disk->{target};
mkdir $dest || die $!;
print "\nMounting $source on $dest with chunkfs\n\n" if ($opts{debug});
my $cmd = "$opts{ionice} $opts{chunkfs} -o fsname=chunkfs-$vm $opts{blocksize} $source $dest 2>/dev/null";
unless( system("$cmd") == 0 ){
die "Couldn't mount $source on $dest\n";
}
}
}
# Remove the dumps
sub run_cleanup{
print "\nRemoving backup files\n" if ($opts{debug});
my $cnt = 0;
my $meta = 0;
my $snap = 0;
# If a state file is present, restore the VM
if (-e "$backupdir/$vm.state"){
restore_vm();
}
# Else, trys to resume it
else{
resume_vm();
}
if (open MOUNTS, "</proc/mounts"){
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});
die "Couldn't unmount $mp\n" unless (
system("/bin/umount $mp 2>/dev/null") == 0
);
rmdir $mp || die $!;
}
close MOUNTS;
}
$cnt = unlink <$backupdir/*>;
if (open SNAPLIST, "<$backupdir.meta/snapshots"){
sleep(1);
foreach (<SNAPLIST>){
# Destroy snapshot listed here is they exists
# and only if the end with _ and 10 digits
chomp;
if ((-e $_) && ($_ =~ m/_\d{10}$/)){
print "Found $_ in snapshot list file, will try to remove it\n" if ($opts{debug});
if (destroy_snapshot($_)){
$snap++;
}
else{
print "An error occured while removing $_\n" if ($opts{debug});
}
}
}
close SNAPLIST;
}
$meta = unlink <$backupdir.meta/*>;
rmdir "$backupdir/";
rmdir "$backupdir.meta";
print "$cnt file(s) removed\n$snap LVM snapshots removed\n$meta metadata files removed\n\n" if $opts{debug};
}
# Print help
sub usage{
print "usage:\n$0 --action=[dump|cleanup|chunkmount|unlock] --vm=vm1[,vm2,vm3] [--debug] [--exclude=hda,hdb] [--compress] ".
"[--state] [--shutdown] [--shutdown-timeout] [--no-snapshot] [--snapsize=<size>] [--backupdir=/path/to/dir] [--connect=<URI>] ".
"[--keep-lock] [--blocksize=<block size>]\n" .
"\n\n" .
"\t--action: What action the script will run. Valid actions are\n\n" .
"\t\t- dump: Run the dump routine (dump disk image to temp dir, pausing the VM if needed). It's the default action\n" .
"\t\t- cleanup: Run the cleanup routine, cleaning up the backup dir\n" .
"\t\t- chunkmount: Mount each device as a chunkfs mount point directly in the backup dir\n" .
"\t\t- unlock: just remove the lock file, but don't cleanup the backup dir\n\n" .
"\t--vm=name: The VM you want to work on (as known by libvirt). You can backup several VMs in one shot " .
"if you separate them with comma, or with multiple --vm argument. You have to use the name of the domain, ".
"ID and UUID are not supported at the moment\n\n" .
"\n\nOther options:\n\n" .
"\t--state: Cleaner way to take backups. If this flag is present, the script will save the current state of " .
"the VM (if running) instead of just suspending it. With this you should be able to restore the VM at " .
"the exact state it was when the backup started. The reason this flag is optional is that some guests " .
"crashes after the restoration, especially when using the kvm-clock. Test this functionnality with" .
"your environnement before using this flag on production. This flag is mutual exclusive with --shutdown\n\n" .
"\t--no-snapshot: Do not attempt to use LVM snapshots. If not present, the script will try to take a snapshot " .
"of each disk of type 'block'. If all disk can be snapshoted, the VM is resumed, or restored (depending " .
"on the --state flag) immediatly after the snapshots have been taken, resulting in almost no downtime. " .
"This is called a \"live backup\" in this script" .
"If at least one disk cannot be snapshoted, the VM is suspended (or stoped) for the time the disks are " .
"dumped in the backup dir. That's why you should use a fast support for the backup dir (fast disks, RAID0 " .
"or RAID10)\n\n" .
"\t--snapsize=<snapsize>: The amount of space to use for snapshots. Use the same format as -L option of lvcreate. " .
"eg: --snapsize=15G. Default is 5G\n\n" .
"\t--compress[=[gzip|bzip2|pbzip2|lzop|xz|lzip|plzip]]: On the fly compress the disks images during the dump. If you " .
"don't specify a compression algo, gzip will be used.\n\n" .
"\t--exclude=hda,hdb: Prevent the disks listed from being dumped. The names are from the VM perspective, as " .
"configured in livirt as the target element. It can be usefull for example if you want to dump the system " .
"disk of a VM, but not the data one which can be backed up separatly, at the files level.\n\n" .
"\t--backupdir=/path/to/backup: Use an alternate backup dir. The directory must exists and be writable. " .
"The default is /var/lib/libvirt/backup\n\n" .
"\t--connect=<URI>: URI to connect to libvirt daemon (to suspend, resume, save, restore VM etc...). " .
"The default is qemu:///system.\n\n" .
"\t--keep-lock: Let the lock file present. This prevent another " .
"dump to run while an third party backup software (BackupPC for example) saves the dumped files.\n\n" .
"\t--shutdown: Shutdown the vm instead of suspending it. This uses ACPI to send the shutdown signal. " .
"You should make sure your guest react to ACPI signals. This flag is mutual exclusive with --state\n\n" .
"\t--shutdown-timeout=<seconds>: How long to wait, in seconds, for the vm to shutdown. If the VM isn't stopped " .
"after that amount of time (in seconds), the backup will abort. The default timeout is 300 seconds\n\n" .
"\t--blocksize=<blocksize>: Specify block size in bytes (for dd and chunkfs). Default to 262144 (256kB).\n";
}
# Save a running VM, if it's running
sub save_vm_state{
if ($dom->is_active()){
print "$vm is running, saving state....\n" if ($opts{debug});
$dom->save("$backupdir/$vm.state");
print "$vm state saved as $backupdir/$vm.state\n" if ($opts{debug});
}
else{
print "$vm is not running, nothing to do\n" if ($opts{debug});
}
}
# Restore the state of a VM
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;
}
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});
}
}
else{
print "\nCannot start domain restoration, $vm is running (maybe already restored after a live backup ?)\n"
if ($opts{debug});
}
}
# Suspend a VM
sub suspend_vm(){
if ($dom->is_active()){
print "$vm is running, suspending\n" if ($opts{debug});
$dom->suspend();
print "$vm now suspended\n" if ($opts{debug});
}
else{
print "$vm is not running, nothing to do\n" if ($opts{debug});
}
}
# Resume a VM if it's paused
sub resume_vm(){
if ($dom->get_info->{state} == Sys::Virt::Domain::STATE_PAUSED){
print "$vm is suspended, resuming\n" if ($opts{debug});
$dom->resume();
print "$vm now resumed\n" if ($opts{debug});
}
else{
print "$vm is not suspended, nothing to do\n" if ($opts{debug});
}
}
# Shutdown a VM via ACPI
sub shutdown_vm(){
if ($dom->is_active()){
print "$vm is running, shutting down\n" if ($opts{debug});
$dom->shutdown();
my $shutdown_counter = 0;
# Wait $opts{shutdowntimeout} seconds for vm to shutdown
while ($dom->get_info->{state} != Sys::Virt::Domain::STATE_SHUTOFF){
if ($shutdown_counter >= $opts{shutdowntimeout}){
die "Waited $opts{shutdown_time} seconds for $vm to shutdown. Shutdown Failed\n";
}
$shutdown_counter++;
sleep(1);
}
}
else{
print "$vm is not running, nothing to do\n" if ($opts{debug});
}
}
sub start_vm(){
if ($dom->get_info->{state} == Sys::Virt::Domain::STATE_SHUTOFF){
print "$vm is shutoff, restarting\n" if ($opts{debug});
$dom->create();
print "$vm started\n" if ($opts{debug});
}
else{
print "$vm is not in a shutdown state, nothing to do\n" if ($opts{debug});
}
}
# Dump the domain description as XML
sub save_xml{
print "\nSaving XML description for $vm to $backupdir/$vm.xml\n" if ($opts{debug});
open(XML, ">$backupdir/$vm" . ".xml") || die $!;
print XML $dom->get_xml_description();
close XML;
}
# Create an LVM snapshot
# Pass the original logical volume and the suffix
# to be added to the snapshot name as arguments
sub create_snapshot{
my ($blk,$suffix) = @_;
my $ret = 0;
print "Running: $opts{lvcreate} -p r -s -n " . $blk . $suffix .
" -L $opts{snapsize} $blk > /dev/null 2>&1\n" if $opts{debug};
if ( system("$opts{lvcreate} -p r -s -n " . $blk . $suffix .
" -L $opts{snapsize} $blk > /dev/null 2>&1") == 0 ) {
$ret = 1;
open SNAPLIST, ">>$backupdir.meta/snapshots" or die "Error, couldn't open snapshot list file\n";
print SNAPLIST $blk.$suffix ."\n";
close SNAPLIST;
}
return $ret;
}
# Remove an LVM snapshot
sub destroy_snapshot{
my $ret = 0;
my ($snap) = @_;
print "Removing snapshot $snap\n" if $opts{debug};
if (system ("$opts{lvremove} -f $snap > /dev/null 2>&1") == 0 ){
$ret = 1;
}
return $ret;
}
# Lock a VM backup dir
# Just creates an empty lock file
sub lock_vm{
print "Locking $vm\n" if $opts{debug};
open ( LOCK, ">$backupdir.meta/$vm.lock" ) || die $!;
print LOCK "";
close LOCK;
}
# Unlock the VM backup dir
# Just removes the lock file
sub unlock_vm{
print "Removing lock file for $vm\n\n" if $opts{debug};
unlink <$backupdir.meta/$vm.lock>;
}