X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FStorage%2FPlugin.pm;h=8a58ff4050ae516af4ffe708a196e8bf274a704a;hb=8f26b3910d7e5149bfa495c3df9c44242af989d5;hp=641ab045a8b85581f6e8b8b72b93f8d4b2c24027;hpb=aefe82ea034b3d796c59034867fe17892ba18080;p=pve-storage.git diff --git a/PVE/Storage/Plugin.pm b/PVE/Storage/Plugin.pm index 641ab04..8a58ff4 100644 --- a/PVE/Storage/Plugin.pm +++ b/PVE/Storage/Plugin.pm @@ -2,20 +2,103 @@ package PVE::Storage::Plugin; use strict; use warnings; + +use Fcntl ':mode'; use File::chdir; use File::Path; +use File::Basename; +use File::stat qw(); + use PVE::Tools qw(run_command); -use PVE::JSONSchema qw(get_standard_option); +use PVE::JSONSchema qw(get_standard_option register_standard_option); use PVE::Cluster qw(cfs_register_file); -use Data::Dumper; +use JSON; use base qw(PVE::SectionConfig); +use constant COMPRESSOR_RE => 'gz|lzo|zst'; + +our @COMMON_TAR_FLAGS = qw( + --one-file-system + -p --sparse --numeric-owner --acls + --xattrs --xattrs-include=user.* --xattrs-include=security.capability + --warning=no-file-ignored --warning=no-xattr-write +); + +our @SHARED_STORAGE = ( + 'iscsi', + 'nfs', + 'cifs', + 'rbd', + 'cephfs', + 'iscsidirect', + 'glusterfs', + 'zfs', + 'drbd'); + +our $MAX_VOLUMES_PER_GUEST = 1024; + cfs_register_file ('storage.cfg', sub { __PACKAGE__->parse_config(@_); }, sub { __PACKAGE__->write_config(@_); }); +my %prune_option = ( + optional => 1, + type => 'integer', minimum => '0', + format_description => 'N', +); + +my $prune_backups_format = { + 'keep-last' => { + %prune_option, + description => 'Keep the last backups.', + }, + 'keep-hourly' => { + %prune_option, + description => 'Keep backups for the last different hours. If there is more' . + 'than one backup for a single hour, only the latest one is kept.' + }, + 'keep-daily' => { + %prune_option, + description => 'Keep backups for the last different days. If there is more' . + 'than one backup for a single day, only the latest one is kept.' + }, + 'keep-weekly' => { + %prune_option, + description => 'Keep backups for the last different weeks. If there is more' . + 'than one backup for a single week, only the latest one is kept.' + }, + 'keep-monthly' => { + %prune_option, + description => 'Keep backups for the last different months. If there is more' . + 'than one backup for a single month, only the latest one is kept.' + }, + 'keep-yearly' => { + %prune_option, + description => 'Keep backups for the last different years. If there is more' . + 'than one backup for a single year, only the latest one is kept.' + }, +}; +PVE::JSONSchema::register_format('prune-backups', $prune_backups_format, \&validate_prune_backups); +sub validate_prune_backups { + my ($keep) = @_; + + die "at least one keep-option must be set and positive\n" + if !grep { $_ } values %{$keep}; + + return $keep; +} +register_standard_option('prune-backups', { + description => "The retention options with shorter intervals are processed first " . + "with --keep-last being the very first one. Each option covers a " . + "specific period of time. We say that backups within this period " . + "are covered by this option. The next option does not take care " . + "of already covered backups and only considers older backups.", + optional => 1, + type => 'string', + format => 'prune-backups', +}); my $defaultData = { propertyList => { @@ -41,6 +124,7 @@ my $defaultData = { minimum => 0, optional => 1, }, + 'prune-backups' => get_standard_option('prune-backups'), shared => { description => "Mark storage as shared.", type => 'boolean', @@ -122,7 +206,7 @@ PVE::JSONSchema::register_format('pve-storage-vgname', \&parse_lvm_name); sub parse_lvm_name { my ($name, $noerr) = @_; - if ($name !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) { + if ($name !~ m/^[a-z0-9][a-z0-9\-\_\.]*[a-z0-9]$/i) { return undef if $noerr; die "lvm name '$name' contains illegal characters\n"; } @@ -305,7 +389,7 @@ sub parse_config { priority => 0, # force first entry path => '/var/lib/vz', maxfiles => 0, - content => { images => 1, rootdir => 1, vztmpl => 1, iso => 1}, + content => { images => 1, rootdir => 1, vztmpl => 1, iso => 1, snippets => 1}, }; } @@ -324,8 +408,7 @@ sub parse_config { if ($def->{content}) { $d->{content} = $def->{content}->[1] if !$d->{content}; } - - if ($type eq 'iscsi' || $type eq 'nfs' || $type eq 'rbd' || $type eq 'sheepdog' || $type eq 'iscsidirect' || $type eq 'glusterfs' || $type eq 'zfs' || $type eq 'drbd') { + if (grep { $_ eq $type } @SHARED_STORAGE) { $d->{shared} = 1; } } @@ -335,6 +418,35 @@ sub parse_config { # Storage implementation +# called during addition of storage (before the new storage config got written) +# die to abort additon if there are (grave) problems +# NOTE: runs in a storage config *locked* context +sub on_add_hook { + my ($class, $storeid, $scfg, %param) = @_; + + # do nothing by default +} + +# called during storage configuration update (before the updated storage config got written) +# die to abort the update if there are (grave) problems +# NOTE: runs in a storage config *locked* context +sub on_update_hook { + my ($class, $storeid, $scfg, %param) = @_; + + # do nothing by default +} + +# called during deletion of storage (before the new storage config got written) +# and if the activate check on addition fails, to cleanup all storage traces +# which on_add_hook may have created. +# die to abort deletion if there are (very grave) problems +# NOTE: runs in a storage config *locked* context +sub on_delete_hook { + my ($class, $storeid, $scfg) = @_; + + # do nothing by default +} + sub cluster_lock_storage { my ($class, $storeid, $shared, $timeout, $func, @param) = @_; @@ -375,18 +487,20 @@ sub parse_volname { my ($vmid, $name) = ($1, $2); my (undef, $format, $isBase) = parse_name_dir($name); return ('images', $name, $vmid, undef, undef, $isBase, $format); - } elsif ($volname =~ m!^iso/([^/]+\.[Ii][Ss][Oo])$!) { + } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::iso_extension_re)$!) { return ('iso', $1); } elsif ($volname =~ m!^vztmpl/([^/]+\.tar\.[gx]z)$!) { return ('vztmpl', $1); } elsif ($volname =~ m!^rootdir/(\d+)$!) { return ('rootdir', $1, $1); - } elsif ($volname =~ m!^backup/([^/]+(\.(tar|tar\.gz|tar\.lzo|tgz|vma|vma\.gz|vma\.lzo)))$!) { + } elsif ($volname =~ m!^backup/([^/]+(?:\.(?:tgz|(?:(?:tar|vma)(?:\.(?:${\COMPRESSOR_RE}))?))))$!) { my $fn = $1; if ($fn =~ m/^vzdump-(openvz|lxc|qemu)-(\d+)-.+/) { return ('backup', $fn, $2); } return ('backup', $fn); + } elsif ($volname =~ m!^snippets/([^/]+)$!) { + return ('snippets', $1); } die "unable to parse directory volume name '$volname'\n"; @@ -398,8 +512,13 @@ my $vtype_subdirs = { iso => 'template/iso', vztmpl => 'template/cache', backup => 'dump', + snippets => 'snippets', }; +sub get_vtype_subdirs { + return $vtype_subdirs; +} + sub get_subdir { my ($class, $scfg, $vtype) = @_; @@ -485,25 +604,57 @@ sub create_base { return $newvolname; } -my $find_free_diskname = sub { - my ($imgdir, $vmid, $fmt) = @_; +my $get_vm_disk_number = sub { + my ($disk_name, $scfg, $vmid, $suffix) = @_; + + my $disk_regex = qr/(vm|base)-$vmid-disk-(\d+)$suffix/; + + my $type = $scfg->{type}; + my $def = { %{$defaultData->{plugindata}->{$type}} }; + + my $valid = $def->{format}[0]; + if ($valid->{subvol}) { + $disk_regex = qr/(vm|base|subvol|basevol)-$vmid-disk-(\d+)/; + } + + if ($disk_name =~ m/$disk_regex/) { + return $2; + } + + return undef; +}; + +sub get_next_vm_diskname { + my ($disk_list, $storeid, $vmid, $fmt, $scfg, $add_fmt_suffix) = @_; + + $fmt //= ''; + my $prefix = ($fmt eq 'subvol') ? 'subvol' : 'vm'; + my $suffix = $add_fmt_suffix ? ".$fmt" : ''; my $disk_ids = {}; - PVE::Tools::dir_glob_foreach($imgdir, - qr!(vm|base)-$vmid-disk-(\d+)\..*!, - sub { - my ($fn, $type, $disk) = @_; - $disk_ids->{$disk} = 1; - }); - - for (my $i = 1; $i < 100; $i++) { + foreach my $disk (@$disk_list) { + my $disknum = $get_vm_disk_number->($disk, $scfg, $vmid, $suffix); + $disk_ids->{$disknum} = 1 if defined($disknum); + } + + for (my $i = 0; $i < $MAX_VOLUMES_PER_GUEST; $i++) { if (!$disk_ids->{$i}) { - return "vm-$vmid-disk-$i.$fmt"; + return "$prefix-$vmid-disk-$i$suffix"; } } - die "unable to allocate a new image name for VM $vmid in '$imgdir'\n"; -}; + die "unable to allocate an image name for VM $vmid in storage '$storeid'\n" +} + +sub find_free_diskname { + my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_; + + my $disks = $class->list_images($storeid, $scfg, $vmid); + + my $disk_list = [ map { $_->{volid} } @$disks ]; + + return get_next_vm_diskname($disk_list, $storeid, $vmid, $fmt, $scfg, $add_fmt_suffix); +} sub clone_image { my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; @@ -527,7 +678,7 @@ sub clone_image { mkpath $imagedir; - my $name = &$find_free_diskname($imagedir, $vmid, "qcow2"); + my $name = $class->find_free_diskname($imagedir, $scfg, $vmid, "qcow2", 1); warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n"; @@ -559,7 +710,7 @@ sub alloc_image { mkpath $imagedir; - $name = &$find_free_diskname($imagedir, $vmid, $fmt) if !$name; + $name = $class->find_free_diskname($imagedir, $scfg, $vmid, $fmt, 1) if !$name; my (undef, $tmpfmt) = parse_name_dir($name); @@ -573,7 +724,7 @@ sub alloc_image { if ($fmt eq 'subvol') { # only allow this if size = 0, so that user knows what he is doing die "storage does not support subvol quotas\n" if $size != 0; - + my $old_umask = umask(0022); my $err; mkdir($path) or $err = "unable to create subvol '$path' - $!\n"; @@ -583,12 +734,17 @@ sub alloc_image { my $cmd = ['/usr/bin/qemu-img', 'create']; push @$cmd, '-o', 'preallocation=metadata' if $fmt eq 'qcow2'; - + push @$cmd, '-f', $fmt, $path, "${size}K"; - run_command($cmd, errmsg => "unable to create image"); + eval { run_command($cmd, errmsg => "unable to create image"); }; + if ($@) { + unlink $path; + rmdir $imagedir; + die "$@"; + } } - + return "$vmid/$name"; } @@ -606,56 +762,47 @@ sub free_image { if (defined($format) && ($format eq 'subvol')) { File::Path::remove_tree($path); } else { - - if (! -f $path) { - warn "disk image '$path' does not exists\n"; + if (!(-f $path || -l $path)) { + warn "disk image '$path' does not exist\n"; return undef; } unlink($path) || die "unlink '$path' failed - $!\n"; } - + + # try to cleanup directory to not clutter storage with empty $vmid dirs if + # all images from a guest got deleted + my $dir = dirname($path); + rmdir($dir); + return undef; } sub file_size_info { my ($filename, $timeout) = @_; - if (-d $filename) { - return wantarray ? (0, 'subvol', 0, undef) : 1; - } - - my $cmd = ['/usr/bin/qemu-img', 'info', $filename]; + my $st = File::stat::stat($filename); - my $format; - my $parent; - my $size = 0; - my $used = 0; + if (S_ISDIR($st->mode)) { + return wantarray ? (0, 'subvol', 0, undef, $st->ctime) : 1; + } + my $json = ''; eval { - run_command($cmd, timeout => $timeout, outfunc => sub { - my $line = shift; - if ($line =~ m/^file format:\s+(\S+)\s*$/) { - $format = $1; - } elsif ($line =~ m/^backing file:\s(\S+)\s/) { - $parent = $1; - } elsif ($line =~ m/^virtual size:\s\S+\s+\((\d+)\s+bytes\)$/) { - $size = int($1); - } elsif ($line =~ m/^disk size:\s+(\d+(.\d+)?)([KMGT])\s*$/) { - $used = $1; - my $u = $3; - - $used *= 1024 if $u eq 'K'; - $used *= (1024*1024) if $u eq 'M'; - $used *= (1024*1024*1024) if $u eq 'G'; - $used *= (1024*1024*1024*1024) if $u eq 'T'; - - $used = int($used); - } - }); + run_command(['/usr/bin/qemu-img', 'info', '--output=json', $filename], + timeout => $timeout, + outfunc => sub { $json .= shift }, + errfunc => sub { warn "$_[0]\n" } + ); }; + warn $@ if $@; + + my $info = eval { decode_json($json) }; + warn "could not parse qemu-img info command output for '$filename'\n" if $@; - return wantarray ? ($size, $format, $used, $parent) : $size; + my ($size, $format, $used, $parent) = $info->@{qw(virtual-size format actual-size backing-filename)}; + + return wantarray ? ($size, $format, $used, $parent, $st->ctime) : $size; } sub volume_size_info { @@ -697,18 +844,10 @@ sub volume_snapshot { return undef; } -sub volume_send { - my ($class, $scfg, $storeid, $volname, $ip, $snap, - $incremental_snap, $verbose, $limit, $target_path) = @_; - - # implement in subclass - die "Volume_send is not implemented for $class"; -} - sub volume_rollback_is_possible { - my ($class, $scfg, $storeid, $volname, $snap) = @_; + my ($class, $scfg, $storeid, $volname, $snap) = @_; - return 1; + return 1; } sub volume_snapshot_rollback { @@ -743,8 +882,14 @@ sub volume_snapshot_delete { return undef; } +sub storage_can_replicate { + my ($class, $scfg, $storeid, $format) = @_; + + return 0; +} + sub volume_has_feature { - my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_; + my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_; my $features = { snapshot => { current => { qcow2 => 1}, snap => { qcow2 => 1} }, @@ -757,6 +902,11 @@ sub volume_has_feature { current => {qcow2 => 1, raw => 1, vmdk => 1} }, }; + # clone_image creates a qcow2 volume + return 0 if $feature eq 'clone' && + defined($opts->{valid_target_formats}) && + !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}}); + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname); @@ -792,7 +942,7 @@ sub list_images { next if !$vollist && defined($vmid) && ($owner ne $vmid); - my ($size, $format, $used, $parent) = file_size_info($fn); + my ($size, $format, $used, $parent, $ctime) = file_size_info($fn); next if !($format && defined($size)); my $volid; @@ -808,10 +958,120 @@ sub list_images { next if !$found; } - push @$res, { + my $info = { volid => $volid, format => $format, size => $size, vmid => $owner, used => $used, parent => $parent }; + + $info->{ctime} = $ctime if $ctime; + + push @$res, $info; + } + + return $res; +} + +# list templates ($tt = ) +my $get_subdir_files = sub { + my ($sid, $path, $tt, $vmid) = @_; + + my $res = []; + + foreach my $fn (<$path/*>) { + + my $st = File::stat::stat($fn); + + next if (!$st || S_ISDIR($st->mode)); + + my $info; + + if ($tt eq 'iso') { + next if $fn !~ m!/([^/]+$PVE::Storage::iso_extension_re)$!i; + + $info = { volid => "$sid:iso/$1", format => 'iso' }; + + } elsif ($tt eq 'vztmpl') { + next if $fn !~ m!/([^/]+\.tar\.([gx]z))$!; + + $info = { volid => "$sid:vztmpl/$1", format => "t$2" }; + + } elsif ($tt eq 'backup') { + next if defined($vmid) && $fn !~ m/\S+-$vmid-\S+/; + next if $fn !~ m!/([^/]+\.(tgz|(?:(?:tar|vma)(?:\.(${\COMPRESSOR_RE}))?)))$!; + my $format = $2; + $fn = $1; + $info = { volid => "$sid:backup/$fn", format => $format }; + + my $archive_info = eval { PVE::Storage::archive_info($fn) } // {}; + + $info->{ctime} = $archive_info->{ctime} if defined($archive_info->{ctime}); + + if (defined($vmid) || $fn =~ m!\-([1-9][0-9]{2,8})\-[^/]+\.${format}$!) { + $info->{vmid} = $vmid // $1; + } + + } elsif ($tt eq 'snippets') { + + $info = { + volid => "$sid:snippets/". basename($fn), + format => 'snippet', + }; + } + + $info->{size} = $st->size; + $info->{ctime} //= $st->ctime; + + push @$res, $info; + } + + return $res; +}; + +sub list_volumes { + my ($class, $storeid, $scfg, $vmid, $content_types) = @_; + + my $res = []; + my $vmlist = PVE::Cluster::get_vmlist(); + foreach my $type (@$content_types) { + my $data; + + if ($type eq 'images' || $type eq 'rootdir') { + $data = $class->list_images($storeid, $scfg, $vmid); + } elsif ($scfg->{path}) { + my $path = $class->get_subdir($scfg, $type); + + if ($type eq 'iso' && !defined($vmid)) { + $data = $get_subdir_files->($storeid, $path, 'iso'); + } elsif ($type eq 'vztmpl'&& !defined($vmid)) { + $data = $get_subdir_files->($storeid, $path, 'vztmpl'); + } elsif ($type eq 'backup') { + $data = $get_subdir_files->($storeid, $path, 'backup', $vmid); + } elsif ($type eq 'snippets') { + $data = $get_subdir_files->($storeid, $path, 'snippets'); + } + } + + next if !$data; + + foreach my $item (@$data) { + if ($type eq 'images' || $type eq 'rootdir') { + my $vminfo = $vmlist->{ids}->{$item->{vmid}}; + my $vmtype; + if (defined($vminfo)) { + $vmtype = $vminfo->{type}; + } + if (defined($vmtype) && $vmtype eq 'lxc') { + $item->{content} = 'rootdir'; + } else { + $item->{content} = 'images'; + } + next if $type ne $item->{content}; + } else { + $item->{content} = $type; + } + + push @$res, $item; + } } return $res; @@ -833,12 +1093,12 @@ sub status { } sub volume_snapshot_list { - my ($class, $scfg, $storeid, $volname, $prefix, $ip) = @_; + my ($class, $scfg, $storeid, $volname) = @_; # implement in subclass die "Volume_snapshot_list is not implemented for $class"; - # retrun an empty array if dataset does not exist. + # return an empty array if dataset does not exist. } sub activate_storage { @@ -848,8 +1108,13 @@ sub activate_storage { die "storage definintion has no path\n" if !$path; - die "unable to activate storage '$storeid' - " . - "directory '$path' does not exist\n" if ! -d $path; + # this path test may hang indefinitely on unresponsive mounts + my $timeout = 2; + if (! PVE::Tools::run_fork_with_timeout($timeout, sub {-d $path})) { + die "unable to activate storage '$storeid' - " . + "directory '$path' does not exist or is unreachable\n"; + } + return if defined($scfg->{mkdir}) && !$scfg->{mkdir}; @@ -871,6 +1136,19 @@ sub deactivate_storage { # do nothing by default } +sub map_volume { + my ($class, $storeid, $scfg, $volname, $snapname) = @_; + + my ($path) = $class->path($scfg, $volname, $storeid, $snapname); + return $path; +} + +sub unmap_volume { + my ($class, $storeid, $scfg, $volname, $snapname) = @_; + + return 1; +} + sub activate_volume { my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; @@ -896,5 +1174,241 @@ sub check_connection { return 1; } +sub prune_backups { + my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_; + + $logfunc //= sub { print "$_[1]\n" }; + + my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']); + + my $backup_groups = {}; + my $prune_list = []; + + foreach my $backup (@{$backups}) { + my $volid = $backup->{volid}; + my $backup_vmid = $backup->{vmid}; + my $archive_info = eval { PVE::Storage::archive_info($volid) } // {}; + my $backup_type = $archive_info->{type} // 'unknown'; + + next if defined($type) && $type ne $backup_type; + + my $prune_entry = { + ctime => $backup->{ctime}, + type => $backup_type, + volid => $volid, + }; + + $prune_entry->{vmid} = $backup_vmid if defined($backup_vmid); + + if ($archive_info->{is_std_name}) { + $prune_entry->{ctime} = $archive_info->{ctime}; + my $group = "$backup_type/$backup_vmid"; + push @{$backup_groups->{$group}}, $prune_entry; + } else { + # ignore backups that don't use the standard naming scheme + $prune_entry->{mark} = 'protected'; + } + + push @{$prune_list}, $prune_entry; + } + + foreach my $backup_group (values %{$backup_groups}) { + PVE::Storage::prune_mark_backup_group($backup_group, $keep); + } + + my $failed; + if (!$dryrun) { + foreach my $prune_entry (@{$prune_list}) { + next if $prune_entry->{mark} ne 'remove'; + + my $volid = $prune_entry->{volid}; + $logfunc->('info', "removing backup '$volid'"); + eval { + my (undef, $volname) = parse_volume_id($volid); + my $archive_path = $class->filesystem_path($scfg, $volname); + PVE::Storage::archive_remove($archive_path); + }; + if (my $err = $@) { + $logfunc->('err', "error when removing backup '$volid' - $err\n"); + $failed = 1; + } + } + } + die "error pruning backups - check log\n" if $failed; + + return $prune_list; +} + +# Import/Export interface: +# Any path based storage is assumed to support 'raw' and 'tar' streams, so +# the default implementations will return this if $scfg->{path} is set, +# mimicking the old PVE::Storage::storage_migrate() function. +# +# Plugins may fall back to PVE::Storage::Plugin::volume_{export,import}... +# functions in case the format doesn't match their specialized +# implementations to reuse the raw/tar code. +# +# Format specification: +# The following formats are all prefixed with image information in the form +# of a 64 bit little endian unsigned integer (pack('Q<')) in order to be able +# to preallocate the image on storages which require it. +# +# raw+size: (image files only) +# A raw binary data stream such as produced via `dd if=TheImageFile`. +# qcow2+size, vmdk: (image files only) +# A raw qcow2/vmdk/... file such as produced via `dd if=some.qcow2` for +# files which are already in qcow2 format, or via `qemu-img convert`. +# Note that these formats are only valid with $with_snapshots being true. +# tar+size: (subvolumes only) +# A GNU tar stream containing just the inner contents of the subvolume. +# This does not distinguish between the contents of a privileged or +# unprivileged container. In other words, this is from the root user +# namespace's point of view with no uid-mapping in effect. +# As produced via `tar -C vm-100-disk-1.subvol -cpf TheOutputFile.dat .` + +# Plugins may reuse these helpers. Changes to the header format should be +# reflected by changes to the function prototypes. +sub write_common_header($$) { + my ($fh, $image_size_in_bytes) = @_; + syswrite($fh, pack("Q<", $image_size_in_bytes), 8); +} + +sub read_common_header($) { + my ($fh) = @_; + sysread($fh, my $size, 8); + $size = unpack('Q<', $size); + die "import: no size found in export header, aborting.\n" if !defined($size); + die "import: got a bad size (not a multiple of 1K), aborting.\n" if ($size&1023); + # Size is in bytes! + return $size; +} + +# Export a volume into a file handle as a stream of desired format. +sub volume_export { + my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_; + if ($scfg->{path} && !defined($snapshot) && !defined($base_snapshot)) { + my $file = $class->path($scfg, $volname, $storeid) + or goto unsupported; + my ($size, $file_format) = file_size_info($file); + + if ($format eq 'raw+size') { + goto unsupported if $with_snapshots || $file_format eq 'subvol'; + write_common_header($fh, $size); + if ($file_format eq 'raw') { + run_command(['dd', "if=$file", "bs=4k"], output => '>&'.fileno($fh)); + } else { + run_command(['qemu-img', 'convert', '-f', $file_format, '-O', 'raw', $file, '/dev/stdout'], + output => '>&'.fileno($fh)); + } + return; + } elsif ($format =~ /^(qcow2|vmdk)\+size$/) { + my $data_format = $1; + goto unsupported if !$with_snapshots || $file_format ne $data_format; + write_common_header($fh, $size); + run_command(['dd', "if=$file", "bs=4k"], output => '>&'.fileno($fh)); + return; + } elsif ($format eq 'tar+size') { + goto unsupported if $file_format ne 'subvol'; + write_common_header($fh, $size); + run_command(['tar', @COMMON_TAR_FLAGS, '-cf', '-', '-C', $file, '.'], + output => '>&'.fileno($fh)); + return; + } + } + unsupported: + die "volume export format $format not available for $class"; +} + +sub volume_export_formats { + my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_; + if ($scfg->{path} && !defined($snapshot) && !defined($base_snapshot)) { + my $file = $class->path($scfg, $volname, $storeid) + or return; + my ($size, $format) = file_size_info($file); + + if ($with_snapshots) { + return ($format.'+size') if ($format eq 'qcow2' || $format eq 'vmdk'); + return (); + } + return ('tar+size') if $format eq 'subvol'; + return ('raw+size'); + } + return (); +} + +# Import data from a stream, creating a new or replacing or adding to an existing volume. +sub volume_import { + my ($class, $scfg, $storeid, $fh, $volname, $format, $base_snapshot, $with_snapshots, $allow_rename) = @_; + + die "volume import format '$format' not available for $class\n" + if $format !~ /^(raw|tar|qcow2|vmdk)\+size$/; + my $data_format = $1; + + die "format $format cannot be imported without snapshots\n" + if !$with_snapshots && ($data_format eq 'qcow2' || $data_format eq 'vmdk'); + die "format $format cannot be imported with snapshots\n" + if $with_snapshots && ($data_format eq 'raw' || $data_format eq 'tar'); + + my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) = + $class->parse_volname($volname); + + # XXX: Should we bother with conversion routines at this level? This won't + # happen without manual CLI usage, so for now we just error out... + die "cannot import format $format into a file of format $file_format\n" + if $data_format ne $file_format && !($data_format eq 'tar' && $file_format eq 'subvol'); + + # Check for an existing file first since interrupting alloc_image doesn't + # free it. + my $file = $class->path($scfg, $volname, $storeid); + if (-e $file) { + die "file '$file' already exists\n" if !$allow_rename; + warn "file '$file' already exists - importing with a different name\n"; + $name = undef; + } + + my ($size) = read_common_header($fh); + $size = int($size/1024); + + eval { + my $allocname = $class->alloc_image($storeid, $scfg, $vmid, $file_format, $name, $size); + my $oldname = $volname; + $volname = $allocname; + if (defined($name) && $allocname ne $oldname) { + die "internal error: unexpected allocated name: '$allocname' != '$oldname'\n"; + } + my $file = $class->path($scfg, $volname, $storeid) + or die "internal error: failed to get path to newly allocated volume $volname\n"; + if ($data_format eq 'raw' || $data_format eq 'qcow2' || $data_format eq 'vmdk') { + run_command(['dd', "of=$file", 'conv=sparse', 'bs=64k'], + input => '<&'.fileno($fh)); + } elsif ($data_format eq 'tar') { + run_command(['tar', @COMMON_TAR_FLAGS, '-C', $file, '-xf', '-'], + input => '<&'.fileno($fh)); + } else { + die "volume import format '$format' not available for $class"; + } + }; + if (my $err = $@) { + eval { $class->free_image($storeid, $scfg, $volname, 0, $file_format) }; + warn $@ if $@; + die $err; + } + + return "$storeid:$volname"; +} + +sub volume_import_formats { + my ($class, $scfg, $storeid, $volname, $base_snapshot, $with_snapshots) = @_; + if ($scfg->{path} && !defined($base_snapshot)) { + my $format = ($class->parse_volname($volname))[6]; + if ($with_snapshots) { + return ($format.'+size') if ($format eq 'qcow2' || $format eq 'vmdk'); + return (); + } + return ('tar+size') if $format eq 'subvol'; + return ('raw+size'); + } + return (); +} 1;