X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FStorage.pm;h=8b9957dedac4fd93e971d0d417f2a759f7242023;hb=a0908caa9983d59d0cd8a9f6a1d2c783ae08d565;hp=7a65624ee4c2d2d06db90c45ccb6dc788c700b2d;hpb=ae36189d26ea64ab0cb174b9991f6ff442554bc7;p=pve-storage.git diff --git a/PVE/Storage.pm b/PVE/Storage.pm index 7a65624..8b9957d 100755 --- a/PVE/Storage.pm +++ b/PVE/Storage.pm @@ -25,8 +25,10 @@ use PVE::Storage::DirPlugin; use PVE::Storage::LVMPlugin; use PVE::Storage::LvmThinPlugin; use PVE::Storage::NFSPlugin; +use PVE::Storage::CIFSPlugin; use PVE::Storage::ISCSIPlugin; use PVE::Storage::RBDPlugin; +use PVE::Storage::CephFSPlugin; use PVE::Storage::SheepdogPlugin; use PVE::Storage::ISCSIDirectPlugin; use PVE::Storage::GlusterfsPlugin; @@ -35,15 +37,21 @@ use PVE::Storage::ZFSPlugin; use PVE::Storage::DRBDPlugin; # Storage API version. Icrement it on changes in storage API interface. -use constant APIVER => 1; +use constant APIVER => 2; +# Age is the number of versions we're backward compatible with. +# This is like having 'current=APIVER' and age='APIAGE' in libtool, +# see https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html +use constant APIAGE => 1; # load standard plugins PVE::Storage::DirPlugin->register(); PVE::Storage::LVMPlugin->register(); PVE::Storage::LvmThinPlugin->register(); PVE::Storage::NFSPlugin->register(); +PVE::Storage::CIFSPlugin->register(); PVE::Storage::ISCSIPlugin->register(); PVE::Storage::RBDPlugin->register(); +PVE::Storage::CephFSPlugin->register(); PVE::Storage::SheepdogPlugin->register(); PVE::Storage::ISCSIDirectPlugin->register(); PVE::Storage::GlusterfsPlugin->register(); @@ -61,18 +69,29 @@ if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) { eval { require $file; + + # Check perl interface: + die "not derived from PVE::Storage::Plugin\n" + if !$modname->isa('PVE::Storage::Plugin'); + die "does not provide an api() method\n" + if !$modname->can('api'); + # Check storage API version and that file is really storage plugin. + my $version = $modname->api(); + die "implements an API version newer than current ($version > " . APIVER . ")\n" + if $version > APIVER; + my $min_version = (APIVER - APIAGE); + die "API version too old, please update the plugin ($version < $min_version)\n" + if $version < $min_version; + import $file; + $modname->register(); + + # If we got this far and the API version is not the same, make some + # noise: + warn "Plugin \"$modname\" is implementing an older storage API, an upgrade is recommended\n" + if $version != APIVER; }; if ($@) { - warn $@; - # Check storage API version and that file is really storage plugin. - } elsif ($modname->isa('PVE::Storage::Plugin') && $modname->can('api') && $modname->api() == APIVER) { - eval { - import $file; - $modname->register(); - }; - warn $@ if $@; - } else { - warn "Error loading storage plugin \"$modname\" because of API version mismatch. Please, update it.\n" + warn "Error loading storage plugin \"$modname\": $@"; } }); } @@ -145,6 +164,17 @@ sub storage_check_enabled { return storage_check_node($cfg, $storeid, $node, $noerr); } +# storage_can_replicate: +# return true if storage supports replication +# (volumes alocated with vdisk_alloc() has replication feature) +sub storage_can_replicate { + my ($cfg, $storeid, $format) = @_; + + my $scfg = storage_config($cfg, $storeid); + my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); + return $plugin->storage_can_replicate($scfg, $storeid, $format); +} + sub storage_ids { my ($cfg) = @_; @@ -525,7 +555,7 @@ sub abs_filesystem_path { } sub storage_migrate { - my ($cfg, $volid, $target_sshinfo, $target_storeid, $target_volname, $base_snapshot, $snapshot, $ratelimit_bps, $insecure) = @_; + my ($cfg, $volid, $target_sshinfo, $target_storeid, $target_volname, $base_snapshot, $snapshot, $ratelimit_bps, $insecure, $with_snapshots, $logfunc) = @_; my ($storeid, $volname) = parse_volume_id($volid); $target_volname = $volname if !$target_volname; @@ -546,186 +576,78 @@ sub storage_migrate { my $ssh_base = PVE::Cluster::ssh_info_to_command_base($target_sshinfo); local $ENV{RSYNC_RSH} = PVE::Tools::cmd2string($ssh_base); - my $no_incremental = sub { - my ($type) = @_; - die "incremental migration not supported on storage type $type\n" - if defined($base_snapshot); - }; - my $no_snapshot = sub { - my ($type) = @_; - # $snapshot is currently only used by replication - die "replicating storage migration not supported on storage type $type\n" - if defined($snapshot); - }; - my @cstream = ([ '/usr/bin/cstream', '-t', $ratelimit_bps ]) if defined($ratelimit_bps); - # only implemented for file system based storage - if ($scfg->{path}) { - $no_incremental->($scfg->{type}); - $no_snapshot->($scfg->{type}); - - if ($tcfg->{path}) { - my $src_plugin = PVE::Storage::Plugin->lookup($scfg->{type}); - my $dst_plugin = PVE::Storage::Plugin->lookup($tcfg->{type}); - my $src = $src_plugin->path($scfg, $volname, $storeid); - my $dst = $dst_plugin->path($tcfg, $target_volname, $target_storeid); - - my $dirname = dirname($dst); - - if ($tcfg->{shared}) { # we can do a local copy - - run_command(['/bin/mkdir', '-p', $dirname]); - - run_command(['/bin/cp', $src, $dst]); - - } else { - run_command([@$ssh, '/bin/mkdir', '-p', $dirname]); - - # we use rsync with --sparse, so we can't use --inplace, - # so we remove file on the target if it already exists to - # save space - my ($size, $format) = PVE::Storage::Plugin::file_size_info($src); - if ($format && ($format eq 'raw') && $size) { - run_command([@$ssh, 'rm', '-f', $dst], - outfunc => sub {}); - } - - my $cmd; - my @bwlimit = ("--bwlimit=${ratelimit_bps}b") if defined($ratelimit_bps); - if ($format eq 'subvol') { - $cmd = ['/usr/bin/rsync', '--progress', '-X', '-A', '--numeric-ids', - '-aH', '--delete', '--no-whole-file', '--inplace', - '--one-file-system', @bwlimit, - "$src/", "[root\@${target_ip}]:$dst"]; - } else { - $cmd = ['/usr/bin/rsync', '--progress', '--sparse', '--whole-file', - @bwlimit, - $src, "[root\@${target_ip}]:$dst"]; - } - - my $percent = -1; - - run_command($cmd, outfunc => sub { - my $line = shift; - - if ($line =~ m/^\s*(\d+\s+(\d+)%\s.*)$/) { - if ($2 > $percent) { - $percent = $2; - print "rsync status: $1\n"; - *STDOUT->flush(); - } - } else { - print "$line\n"; - *STDOUT->flush(); - } - }); - } - } else { - die "$errstr - target type '$tcfg->{type}' not implemented\n"; + my $migration_snapshot; + if (!defined($snapshot)) { + if ($scfg->{type} eq 'zfspool') { + $migration_snapshot = 1; + $snapshot = '__migration__'; } + } - } elsif ($scfg->{type} eq 'zfspool') { - - if ($tcfg->{type} eq 'zfspool') { - - die "$errstr - pool on target does not have the same name as on source!" - if $tcfg->{pool} ne $scfg->{pool}; - - my $migration_snapshot; - if (!defined($snapshot)) { - $migration_snapshot = 1; - $snapshot = '__migration__'; - } - - my (undef, $volname) = parse_volname($cfg, $volid); - my $zfspath = "$scfg->{pool}\/$volname"; - - my @formats = volume_transfer_formats($cfg, $volid, $volid, $snapshot, $base_snapshot, 1); - die "cannot migrate from storage type '$scfg->{type}' to '$tcfg->{type}'\n" if !@formats; - my $format = $formats[0]; + my @formats = volume_transfer_formats($cfg, $volid, $volid, $snapshot, $base_snapshot, $with_snapshots); + die "cannot migrate from storage type '$scfg->{type}' to '$tcfg->{type}'\n" if !@formats; + my $format = $formats[0]; - my @insecurecmd; - if ($insecure) { - @insecurecmd = ('pvecm', 'mtunnel', '-run-command', 1); - if (my $network = $target_sshinfo->{network}) { - push @insecurecmd, '-migration_network', $network; - } - } + my @insecurecmd; + if ($insecure) { + @insecurecmd = ('pvecm', 'mtunnel', '-run-command', 1); + if (my $network = $target_sshinfo->{network}) { + push @insecurecmd, '-migration_network', $network; + } + } - my $send = ['pvesm', 'export', $volid, $format, '-', '-snapshot', $snapshot, '-with-snapshots', '1']; - my $recv = [@$ssh, @insecurecmd, '--', 'pvesm', 'import', $volid, $format, '-', '-with-snapshots', '1']; - if ($migration_snapshot) { - push @$recv, '-delete-snapshot', $snapshot; - } + $with_snapshots = $with_snapshots ? 1 : 0; # sanitize for passing as cli parameter + my $send = ['pvesm', 'export', $volid, $format, '-', '-with-snapshots', $with_snapshots]; + my $recv = [@$ssh, @insecurecmd, '--', 'pvesm', 'import', $volid, $format, '-', '-with-snapshots', $with_snapshots]; + if (defined($snapshot)) { + push @$send, '-snapshot', $snapshot + } + if ($migration_snapshot) { + push @$recv, '-delete-snapshot', $snapshot; + } - if (defined($base_snapshot)) { - # Check if the snapshot exists on the remote side: - push @$send, '-base', $base_snapshot; - push @$recv, '-base', $base_snapshot; - } + if (defined($base_snapshot)) { + # Check if the snapshot exists on the remote side: + push @$send, '-base', $base_snapshot; + push @$recv, '-base', $base_snapshot; + } - volume_snapshot($cfg, $volid, $snapshot) if $migration_snapshot; - eval { - if ($insecure) { - my $pid = open(my $info, '-|', @$recv) - or die "receive command failed: $!\n"; - my ($ip) = <$info> =~ /^($PVE::Tools::IPRE)$/ or die "no tunnel IP received\n"; - my ($port) = <$info> =~ /^(\d+)$/ or die "no tunnel port received\n"; - my $socket = IO::Socket::IP->new(PeerHost => $ip, PeerPort => $port, Type => SOCK_STREAM) - or die "failed to connect to tunnel at $ip:$port\n"; - run_command([$send, @cstream], output => '>&'.fileno($socket)); - } else { - run_command([$send, @cstream, $recv]); - } - }; - my $err = $@; - warn "send/receive failed, cleaning up snapshot(s)..\n" if $err; - if ($migration_snapshot) { - eval { volume_snapshot_delete($cfg, $volid, $snapshot, 0) }; - warn "could not remove source snapshot: $@\n" if $@; - } - die $err if $err; - } else { - die "$errstr - target type $tcfg->{type} is not valid\n"; - } - - } elsif ($scfg->{type} eq 'lvmthin' || $scfg->{type} eq 'lvm') { - $no_incremental->($scfg->{type}); - $no_snapshot->($scfg->{type}); - - if (($scfg->{type} eq $tcfg->{type}) && - ($tcfg->{type} eq 'lvmthin' || $tcfg->{type} eq 'lvm')) { - - my (undef, $volname, $vmid) = parse_volname($cfg, $volid); - my $size = volume_size_info($cfg, $volid, 5); - my $src = path($cfg, $volid); - my $dst = path($cfg, $target_volid); - - run_command([@$ssh, '--', - 'pvesm', 'alloc', $target_storeid, $vmid, - $target_volname, int($size/1024)]); - - eval { - if ($tcfg->{type} eq 'lvmthin') { - run_command([["dd", "if=$src", "bs=4k"], @cstream, - [@$ssh, "dd", 'conv=sparse', "of=$dst", "bs=4k"]]); - } else { - run_command([["dd", "if=$src", "bs=4k"], @cstream, - [@$ssh, "dd", "of=$dst", "bs=4k"]]); - } - }; - if (my $err = $@) { - run_command([@$ssh, 'pvesm', 'free', $target_volid]); - die $err; + volume_snapshot($cfg, $volid, $snapshot) if $migration_snapshot; + eval { + if ($insecure) { + open(my $info, '-|', @$recv) + or die "receive command failed: $!\n"; + my ($ip) = <$info> =~ /^($PVE::Tools::IPRE)$/ or die "no tunnel IP received\n"; + my ($port) = <$info> =~ /^(\d+)$/ or die "no tunnel port received\n"; + my $socket = IO::Socket::IP->new(PeerHost => $ip, PeerPort => $port, Type => SOCK_STREAM) + or die "failed to connect to tunnel at $ip:$port\n"; + # we won't be reading from the socket + shutdown($socket, 0); + run_command([$send, @cstream], output => '>&'.fileno($socket)); + # don't close the connection entirely otherwise the receiving end + # might not get all buffered data (and fails with 'connection reset by peer') + shutdown($socket, 1); + 1 while <$info>; # wait for the remote process to finish + # now close the socket + close($socket); + if (!close($info)) { # does waitpid() + die "import failed: $!\n" if $!; + die "import failed: exit code ".($?>>8)."\n"; } } else { - die "$errstr - migrate from source type '$scfg->{type}' to '$tcfg->{type}' not implemented\n"; + run_command([$send, @cstream, $recv], logfunc => $logfunc); } - } else { - die "$errstr - source type '$scfg->{type}' not implemented\n"; + }; + my $err = $@; + warn "send/receive failed, cleaning up snapshot(s)..\n" if $err; + if ($migration_snapshot) { + eval { volume_snapshot_delete($cfg, $volid, $snapshot, 0) }; + warn "could not remove source snapshot: $@\n" if $@; } + die $err if $err; } sub vdisk_clone { @@ -764,6 +686,30 @@ sub vdisk_create_base { }); } +sub map_volume { + my ($cfg, $volid, $snapname) = @_; + + my ($storeid, $volname) = parse_volume_id($volid); + + my $scfg = storage_config($cfg, $storeid); + + my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); + + return $plugin->map_volume($storeid, $scfg, $volname, $snapname); +} + +sub unmap_volume { + my ($cfg, $volid, $snapname) = @_; + + my ($storeid, $volname) = parse_volume_id($volid); + + my $scfg = storage_config($cfg, $storeid); + + my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); + + return $plugin->unmap_volume($storeid, $scfg, $volname, $snapname); +} + sub vdisk_alloc { my ($cfg, $storeid, $vmid, $fmt, $name, $size) = @_; @@ -1098,7 +1044,7 @@ sub deactivate_volumes { } sub storage_info { - my ($cfg, $content) = @_; + my ($cfg, $content, $includeformat) = @_; my $ids = $cfg->{ids}; @@ -1108,8 +1054,7 @@ sub storage_info { my $slist = []; foreach my $storeid (keys %$ids) { - - next if !storage_check_enabled($cfg, $storeid, undef, 1); + my $storage_enabled = defined(storage_check_enabled($cfg, $storeid, undef, 1)); if (defined($content)) { my $want_ctype = 0; @@ -1119,7 +1064,7 @@ sub storage_info { last; } } - next if !$want_ctype; + next if !$want_ctype || !$storage_enabled; } my $type = $ids->{$storeid}->{type}; @@ -1132,6 +1077,7 @@ sub storage_info { shared => $ids->{$storeid}->{shared} ? 1 : 0, content => PVE::Storage::Plugin::content_hash_to_string($ids->{$storeid}->{content}), active => 0, + enabled => $storage_enabled ? 1 : 0, }; push @$slist, $storeid; @@ -1141,7 +1087,18 @@ sub storage_info { foreach my $storeid (keys %$ids) { my $scfg = $ids->{$storeid}; + next if !$info->{$storeid}; + next if !$info->{$storeid}->{enabled}; + + my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); + if ($includeformat) { + my $pd = $plugin->plugindata(); + $info->{$storeid}->{format} = $pd->{format} + if $pd->{format}; + $info->{$storeid}->{select_existing} = $pd->{select_existing} + if $pd->{select_existing}; + } eval { activate_storage($cfg, $storeid, $cache); }; if (my $err = $@) { @@ -1149,9 +1106,7 @@ sub storage_info { next; } - my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); - my ($total, $avail, $used, $active); - eval { ($total, $avail, $used, $active) = $plugin->status($storeid, $scfg, $cache); }; + my ($total, $avail, $used, $active) = eval { $plugin->status($storeid, $scfg, $cache); }; warn $@ if $@; next if !$active; $info->{$storeid}->{total} = int($total); @@ -1201,6 +1156,41 @@ sub scan_nfs { return $res; } +sub scan_cifs { + my ($server_in, $user, $password, $domain) = @_; + + my $server; + if (!($server = resolv_server ($server_in))) { + die "unable to resolve address for server '${server_in}'\n"; + } + + # we support only Windows grater than 2012 cifsscan so use smb3 + my $cmd = ['/usr/bin/smbclient', '-m', 'smb3', '-d', '0', '-L', $server]; + if (defined($user)) { + die "password is required" if !defined($password); + push @$cmd, '-U', "$user\%$password"; + push @$cmd, '-W', $domain if defined($domain); + } else { + push @$cmd, '-N'; + } + + my $res = {}; + run_command($cmd, + outfunc => sub { + my $line = shift; + if ($line =~ m/(\S+)\s*Disk\s*(\S*)/) { + $res->{$1} = $2; + } elsif ($line =~ m/(NT_STATUS_(\S*))/) { + $res->{$1} = ''; + } + }, + errfunc => sub {}, + noerr => 1 + ); + + return $res; +} + sub scan_zfs { my $cmd = ['zfs', 'list', '-t', 'filesystem', '-H', '-o', 'name,avail,used']; @@ -1621,4 +1611,91 @@ sub complete_volume { return $res; } +# Various io-heavy operations require io/bandwidth limits which can be +# configured on multiple levels: The global defaults in datacenter.cfg, and +# per-storage overrides. When we want to do a restore from storage A to storage +# B, we should take the smaller limit defined for storages A and B, and if no +# such limit was specified, use the one from datacenter.cfg. +sub get_bandwidth_limit { + my ($operation, $storage_list, $override) = @_; + + # called for each limit (global, per-storage) with the 'default' and the + # $operation limit and should udpate $override for every limit affecting + # us. + my $use_global_limits = 0; + my $apply_limit = sub { + my ($bwlimit) = @_; + if (defined($bwlimit)) { + my $limits = PVE::JSONSchema::parse_property_string('bwlimit', $bwlimit); + my $limit = $limits->{$operation} // $limits->{default}; + if (defined($limit)) { + if (!$override || $limit < $override) { + $override = $limit; + } + return; + } + } + # If there was no applicable limit, try to apply the global ones. + $use_global_limits = 1; + }; + + my ($rpcenv, $authuser); + if (defined($override)) { + $rpcenv = PVE::RPCEnvironment->get(); + $authuser = $rpcenv->get_user(); + } + + # Apply per-storage limits - if there are storages involved. + if (@$storage_list) { + my $config = config(); + + # The Datastore.Allocate permission allows us to modify the per-storage + # limits, therefore it also allows us to override them. + # Since we have most likely multiple storages to check, do a quick check on + # the general '/storage' path to see if we can skip the checks entirely: + return $override if $rpcenv && $rpcenv->check($authuser, '/storage', ['Datastore.Allocate'], 1); + + my %done; + foreach my $storage (@$storage_list) { + # Avoid duplicate checks: + next if $done{$storage}; + $done{$storage} = 1; + + # Otherwise we may still have individual /storage/$ID permissions: + if (!$rpcenv || !$rpcenv->check($authuser, "/storage/$storage", ['Datastore.Allocate'], 1)) { + # And if not: apply the limits. + my $storecfg = storage_config($config, $storage); + $apply_limit->($storecfg->{bwlimit}); + } + } + + # Storage limits take precedence over the datacenter defaults, so if + # a limit was applied: + return $override if !$use_global_limits; + } + + # Sys.Modify on '/' means we can change datacenter.cfg which contains the + # global default limits. + if (!$rpcenv || !$rpcenv->check($authuser, '/', ['Sys.Modify'], 1)) { + # So if we cannot modify global limits, apply them to our currently + # requested override. + my $dc = cfs_read_file('datacenter.cfg'); + $apply_limit->($dc->{bwlimit}); + } + + return $override; +} + +# checks if the storage id is available and dies if not +sub assert_sid_unused { + my ($sid) = @_; + + my $cfg = config(); + if (my $scfg = storage_config($cfg, $sid, 1)) { + die "storage ID '$sid' already defined\n"; + } + + return undef; +} + 1;