Mounting XenServer .xva in place
With a bunch of VM backups as XenServer exported .xva files I found sometimes I just wanted a file and not looking to restore the whole VM. After digging into the file format a bit I put together xvatodisk.pl which will allow you to mount (read only) the disks in an XVA, in place. It does so by mapping out the XVA and using device-mapper to make the device.
You need dmsetup and partprobe installed, on Debian: apt-get install dmsetup parted && modprobe dm_mod
XVA Format
An XVA is just a tar with a structure of:
ova.xml
Ref:1234/
00000000
00000000.checksum
00000010
00000010.checksum
...
Ref:567/
00000000
00000000.checksum
...
ova.xml describes the virtual machine. For every disk in the VM there is a Ref:<\d+> directory. In that directory are 1 MB chunks (\d{8}) and an SHA1 checksum for that chunk (\d{8}.checksum).
There will always be a first and last chunk for the disk. Gaps in the numbering are for chunks that are empty. Sometimes there is a chunk with a size of 0 but I haven’t seen any that are in between 0 and 1048576 bytes.
#!/usr/bin/env perl
use strict;
use warnings;
use Fcntl qw/SEEK_SET/;
use Storable qw/store retrieve/;
use File::Temp;
use Getopt::Long;
use Pod::Usage;
use constant CHUNK_SIZE => 1048576;
use constant SECTOR_SIZE => 512;
=head1 NAME
xvatodisk - Makes disks in a XVA file available for mounting read-only.
=head1 SYNOPSIS
xvatodisk -x </path/to/exported.xva> [-m </path/to/saved.map>]
=over
=item arguments:
-h, --help display this help
-x, --xva path to xva to use
-m, --map path to map of xva to use or save
=back
=head1 DESCRIPTION
=cut
my ($xva_file, $xva_map_file, $help);
my $opt = GetOptions(
"xva|x=s" => \$xva_file,
"map|m:s" => \$xva_map_file,
"help|h" => \$help
);
pod2usage(1) if (!$opt || $help || !defined($xva_file) || !-e $xva_file);
# Mapping the xva can take a while on large files, save the map.
$xva_map_file ||= $xva_file . "-map";
my $xva = (-e $xva_map_file) ? retrieve($xva_map_file) : make_xva_map($xva_file, $xva_map_file);
# There can be multiple disks in an xva.
my $disks_found = scalar keys %$xva;
die "No disks were found in $xva_file" if ($disks_found == 0);
# Only need 1 loop device for all the disks.
chomp(my $loop_dev = `losetup --find`);
system("losetup", "--read-only", $loop_dev, $xva_file);
for my $id (keys %$xva) {
printf "Disk Ref:$id (%.02f GB) -> /dev/mapper/xva-$id\n", $#{$xva->{$id}} / 1024;
my $tmp = File::Temp->new(TEMPLATE => "dmtable-XXXX", SUFFIX => ".map");
print $tmp make_dmtable($xva->{$id}, $loop_dev);
system("dmsetup", "--readonly", "create", "xva-$id", $tmp->filename);
`partprobe /dev/mapper/xva-$id 2>&1 > /dev/null`;
}
# Try and close things out on ctrl-c.
$SIG{INT} = sub {
print "\ncleaning up...\n";
for my $id (keys %$xva) {
for (`ls -fXr /dev/mapper/xva-$id*`) {
chomp();
system("dmsetup", "remove", $_);
}
}
system("losetup", "--detach", $loop_dev);
exit;
};
sleep 1 while 1;
#-------------------------------------------------------------------------------
# Returns a table for dmtable that maps the disks.
#-------------------------------------------------------------------------------
sub make_dmtable {
my ($disk_map, $loop_dev) = @_;
my $table = "";
my $sector = 0;
my $total_chunks = scalar(@$disk_map);
for (my $chunk_num = 0; $chunk_num < $total_chunks; $chunk_num++) {
if (defined(my $chunk = $disk_map->[$chunk_num])) {
# Offset is at ->[1], size at ->[0]
# Checksum files follow each chunk preventing continuos data.
$table .= sprintf "%i %i %s %s %i\n",
$sector, $chunk->[1] / SECTOR_SIZE, "linear",
$loop_dev, $chunk->[0] / SECTOR_SIZE;
$sector += $chunk->[1] / SECTOR_SIZE;
}
else {
# Map out a zero section, safe because the last chunk of a disk
# will always exist.
my $empty_chunks = 1;
$empty_chunks++ until (defined($disk_map->[++$chunk_num]));
$chunk_num--;
my $empty_sectors = $empty_chunks * (CHUNK_SIZE / SECTOR_SIZE);
$table .= sprintf "%i %i %s\n", $sector, $empty_sectors, "zero";
$sector += $empty_sectors;
}
}
return $table;
}
#-------------------------------------------------------------------------------
# Returns a hashref that maps out each disk in the xva and the location of
# every non-zero sized chunk of data.
#-------------------------------------------------------------------------------
sub make_xva_map {
my ($xva_file, $xva_map_file) = @_;
open(my $fh, "<:raw", $xva_file);
my %xva;
my $xva_size = (stat $xva_file)[7];
my $offset = 0;
while (my $hdr = read_tar_header($fh, $offset)) {
printf(" - indexing xva: %0.2f\r", ($offset * 100) / $xva_size);
$offset += 512;
# Ignore the empty chunks since this will be read only.
if ($hdr->{name} =~ m{^Ref:(\d+)/(\d+)\0+$} && $hdr->{size} != 0) {
$xva{$1}[$2] = [$offset, $hdr->{size}];
}
$offset += $hdr->{size} + $hdr->{padding};
}
close($fh);
print " indexing xva complete\n";
print " map saved to: $xva_map_file\n";
store(\%xva, $xva_map_file);
return \%xva;
}
#-------------------------------------------------------------------------------
# Returns a hashref with the tar header at $offset or the current file
# position. Returns undef at the end of the tar. This will advance the file
# position by 512 bytes.
#-------------------------------------------------------------------------------
sub read_tar_header {
my ($fh, $offset) = @_;
sysseek($fh, $offset, SEEK_SET) if (defined($offset));
sysread($fh, my $tar, 512);
# The last 1024+ bytes of a tar are 0 so a 0 for the filename
# should catch end of the tar.
return undef if (ord(substr($tar, 0, 1)) == 0);
# Numbers in the tar header are stored in ascii octal.
my $size = oct(substr($tar, 124, 11));
return {
name => substr($tar, 0, 99),
size => $size,
padding => (($size + 511) & ~511) - $size,
};
}