X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FCLI%2Fpvesm.pm;h=8f7740e9ed145ce675d747cd5f1616ced102727e;hb=11a942a18a9689bf97bb76b8f3570c5dfc8e61ae;hp=376781059e32fb57ed6c85b74c09a67a23b0b7ce;hpb=f984732e0ea14039926d6464f38014ca8fb87045;p=pve-storage.git diff --git a/PVE/CLI/pvesm.pm b/PVE/CLI/pvesm.pm index 3767810..8f7740e 100755 --- a/PVE/CLI/pvesm.pm +++ b/PVE/CLI/pvesm.pm @@ -3,6 +3,7 @@ package PVE::CLI::pvesm; use strict; use warnings; +use POSIX qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC); use Fcntl ':flock'; use File::Path; @@ -11,22 +12,82 @@ use PVE::Cluster; use PVE::INotify; use PVE::RPCEnvironment; use PVE::Storage; +use PVE::Tools qw(extract_param); use PVE::API2::Storage::Config; use PVE::API2::Storage::Content; -use PVE::API2::Storage::Status; +use PVE::API2::Storage::PruneBackups; use PVE::API2::Storage::Scan; +use PVE::API2::Storage::Status; use PVE::JSONSchema qw(get_standard_option); +use PVE::PTY; use PVE::CLIHandler; use base qw(PVE::CLIHandler); +my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs']; + my $nodename = PVE::INotify::nodename(); +sub param_mapping { + my ($name) = @_; + + my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', { + func => sub { + my ($value) = @_; + return $value if $value; + return PVE::PTY::read_password("Enter Password: "); + }, + }); + + my $enc_key_map = { + name => 'encryption-key', + desc => 'a file containing an encryption key, or the special value "autogen"', + func => sub { + my ($value) = @_; + return $value if $value eq 'autogen'; + return PVE::Tools::file_get_contents($value); + } + }; + + + my $mapping = { + 'cifsscan' => [ $password_map ], + 'cifs' => [ $password_map ], + 'create' => [ $password_map, $enc_key_map ], + 'update' => [ $password_map, $enc_key_map ], + }; + return $mapping->{$name}; +} + sub setup_environment { PVE::RPCEnvironment->setup_default_cli_env(); } +__PACKAGE__->register_method ({ + name => 'apiinfo', + path => 'apiinfo', + method => 'GET', + description => "Returns APIVER and APIAGE.", + parameters => { + additionalProperties => 0, + properties => {}, + }, + returns => { + type => 'object', + properties => { + apiver => { type => 'integer' }, + apiage => { type => 'integer' }, + }, + }, + code => sub { + return { + apiver => PVE::Storage::APIVER, + apiage => PVE::Storage::APIAGE, + }; + } +}); + __PACKAGE__->register_method ({ name => 'path', path => 'path', @@ -86,7 +147,7 @@ __PACKAGE__->register_method ({ my $authuser = $rpcenv->get_user(); my $storage_cfg = PVE::Storage::config(); - $rpcenv->check_volume_access($authuser, $storage_cfg, undef, $volume); + PVE::Storage::check_volume_access($rpcenv, $authuser, $storage_cfg, undef, $volume); my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume); @@ -97,28 +158,30 @@ __PACKAGE__->register_method ({ my $print_content = sub { my ($list) = @_; - my $maxlenname = 0; + my ($maxlenname, $maxsize) = (0, 0); foreach my $info (@$list) { - my $volid = $info->{volid}; my $sidlen = length ($volid); $maxlenname = $sidlen if $sidlen > $maxlenname; + $maxsize = $info->{size} if ($info->{size} // 0) > $maxsize; } + my $sizemaxdigits = length($maxsize); + + my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s"; + printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID"; foreach my $info (@$list) { next if !$info->{vmid}; my $volid = $info->{volid}; - printf "%-${maxlenname}s %5s %10d %d\n", $volid, - $info->{format}, $info->{size}, $info->{vmid}; + printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid}; } foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) { next if $info->{vmid}; my $volid = $info->{volid}; - printf "%-${maxlenname}s %5s %10d\n", $volid, - $info->{format}, $info->{size}; + printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $info->{size}; } }; @@ -132,18 +195,317 @@ my $print_status = sub { } $maxlen+=1; + printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type', + 'Status', 'Total', 'Used', 'Available', '%'; + foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) { my $storeid = $res->{storage}; - my $sum = $res->{used} + $res->{avail}; - my $per = $sum ? (0.5 + ($res->{used}*100)/$sum) : 100; + my $active = $res->{active} ? 'active' : 'inactive'; + my ($per, $per_fmt) = (0, '% 7.2f%%'); + $per = ($res->{used}*100)/$res->{total} if $res->{total} > 0; + + if (!$res->{enabled}) { + $per = 'N/A'; + $per_fmt = '% 8s'; + $active = 'disabled'; + } - printf "%-${maxlen}s %5s %1d %15d %15d %15d %.2f%%\n", $storeid, - $res->{type}, $res->{active}, - $res->{total}/1024, $res->{used}/1024, $res->{avail}/1024, $per; + printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid, + $res->{type}, $active, $res->{total}/1024, $res->{used}/1024, + $res->{avail}/1024, $per; } }; +__PACKAGE__->register_method ({ + name => 'export', + path => 'export', + method => 'GET', + description => "Used internally to export a volume.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + volume => { + description => "Volume identifier", + type => 'string', + completion => \&PVE::Storage::complete_volume, + }, + format => { + description => "Export stream format", + type => 'string', + enum => $KNOWN_EXPORT_FORMATS, + }, + filename => { + description => "Destination file name", + type => 'string', + }, + base => { + description => "Snapshot to start an incremental stream from", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,40}/, + maxLength => 40, + optional => 1, + }, + snapshot => { + description => "Snapshot to export", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,40}/, + maxLength => 40, + optional => 1, + }, + 'with-snapshots' => { + description => + "Whether to include intermediate snapshots in the stream", + type => 'boolean', + optional => 1, + default => 0, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $filename = $param->{filename}; + + my $outfh; + if ($filename eq '-') { + $outfh = \*STDOUT; + } else { + sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC) + or die "open($filename): $!\n"; + } + + eval { + my $cfg = PVE::Storage::config(); + PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format}, + $param->{snapshot}, $param->{base}, $param->{'with-snapshots'}); + }; + my $err = $@; + if ($filename ne '-') { + close($outfh); + unlink($filename) if $err; + } + die $err if $err; + return; + } +}); + +__PACKAGE__->register_method ({ + name => 'import', + path => 'import', + method => 'PUT', + description => "Used internally to import a volume.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + volume => { + description => "Volume identifier", + type => 'string', + completion => \&PVE::Storage::complete_volume, + }, + format => { + description => "Import stream format", + type => 'string', + enum => $KNOWN_EXPORT_FORMATS, + }, + filename => { + description => "Source file name. For '-' stdin is used, the " . + "tcp:// format allows to use a TCP connection as input. " . + "Else, the file is treated as common file.", + type => 'string', + }, + base => { + description => "Base snapshot of an incremental stream", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,40}/, + maxLength => 40, + optional => 1, + }, + 'with-snapshots' => { + description => + "Whether the stream includes intermediate snapshots", + type => 'boolean', + optional => 1, + default => 0, + }, + 'delete-snapshot' => { + description => "A snapshot to delete on success", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,80}/, + maxLength => 80, + optional => 1, + }, + 'allow-rename' => { + description => "Choose a new volume ID if the requested " . + "volume ID already exists, instead of throwing an error.", + type => 'boolean', + optional => 1, + default => 0, + }, + }, + }, + returns => { type => 'string' }, + code => sub { + my ($param) = @_; + + my $filename = $param->{filename}; + + my $infh; + if ($filename eq '-') { + $infh = \*STDIN; + } elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) { + my ($cidr, $ip, $subnet) = ($1, $2, $3); + if ($subnet) { # got real CIDR notation, not just IP + my $ips = PVE::Network::get_local_ip_from_cidr($cidr); + die "Unable to get any local IP address in network '$cidr'\n" + if scalar(@$ips) < 1; + die "Got multiple local IP address in network '$cidr'\n" + if scalar(@$ips) > 1; + + $ip = $ips->[0]; + } + my $family = PVE::Tools::get_host_address_family($ip); + my $port = PVE::Tools::next_migrate_port($family, $ip); + + my $sock_params = { + Listen => 1, + ReuseAddr => 1, + Proto => &Socket::IPPROTO_TCP, + GetAddrInfoFlags => 0, + LocalAddr => $ip, + LocalPort => $port, + }; + my $socket = IO::Socket::IP->new(%$sock_params) + or die "failed to open socket: $!\n"; + + print "$ip\n$port\n"; # tell remote where to connect + *STDOUT->flush(); + + my $prev_alarm = alarm 0; + local $SIG{ALRM} = sub { die "timed out waiting for client\n" }; + alarm 30; + my $client = $socket->accept; # Wait for a client + alarm $prev_alarm; + close($socket); + + $infh = \*$client; + } else { + sysopen($infh, $filename, O_RDONLY) + or die "open($filename): $!\n"; + } + + my $cfg = PVE::Storage::config(); + my $volume = $param->{volume}; + my $delete = $param->{'delete-snapshot'}; + my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format}, + $param->{base}, $param->{'with-snapshots'}, $param->{'allow-rename'}); + PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete) + if defined($delete); + return $imported_volid; + } +}); + +__PACKAGE__->register_method ({ + name => 'prunebackups', + path => 'prunebackups', + method => 'GET', + description => "Prune backups. Only those using the standard naming scheme are considered. " . + "If no keep options are specified, those from the storage configuration are used.", + protected => 1, + proxyto => 'node', + parameters => { + additionalProperties => 0, + properties => { + 'dry-run' => { + description => "Only show what would be pruned, don't delete anything.", + type => 'boolean', + optional => 1, + }, + node => get_standard_option('pve-node'), + storage => get_standard_option('pve-storage-id', { + completion => \&PVE::Storage::complete_storage_enabled, + }), + %{$PVE::Storage::Plugin::prune_backups_format}, + type => { + description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.", + type => 'string', + optional => 1, + enum => ['qemu', 'lxc'], + }, + vmid => get_standard_option('pve-vmid', { + description => "Only consider backups for this guest.", + optional => 1, + completion => \&PVE::Cluster::complete_vmid, + }), + }, + }, + returns => { + type => 'object', + properties => { + dryrun => { + description => 'If it was a dry run or not. The list will only be defined in that case.', + type => 'boolean', + }, + list => { + type => 'array', + items => { + type => 'object', + properties => { + volid => { + description => "Backup volume ID.", + type => 'string', + }, + 'ctime' => { + description => "Creation time of the backup (seconds since the UNIX epoch).", + type => 'integer', + }, + 'mark' => { + description => "Whether the backup would be kept or removed. For backups that don't " . + "use the standard naming scheme, it's 'protected'.", + type => 'string', + }, + type => { + description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.", + type => 'string', + }, + 'vmid' => { + description => "The VM the backup belongs to.", + type => 'integer', + optional => 1, + }, + }, + }, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $dryrun = extract_param($param, 'dry-run') ? 1 : 0; + + my $keep_opts; + foreach my $keep (keys %{$PVE::Storage::Plugin::prune_backups_format}) { + $keep_opts->{$keep} = extract_param($param, $keep) if defined($param->{$keep}); + } + $param->{'prune-backups'} = PVE::JSONSchema::print_property_string( + $keep_opts, $PVE::Storage::Plugin::prune_backups_format) if $keep_opts; + + my $list = []; + if ($dryrun) { + $list = PVE::API2::Storage::PruneBackups->dryrun($param); + } else { + PVE::API2::Storage::PruneBackups->delete($param); + } + + return { + dryrun => $dryrun, + list => $list, + }; + }}); + our $cmddef = { add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ], set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ], @@ -159,64 +521,126 @@ our $cmddef = { }], free => [ "PVE::API2::Storage::Content", 'delete', ['volume'], { node => $nodename } ], - nfsscan => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'], - { node => $nodename }, sub { - my $res = shift; - - my $maxlen = 0; - foreach my $rec (@$res) { - my $len = length ($rec->{path}); - $maxlen = $len if $len > $maxlen; - } - foreach my $rec (@$res) { - printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options}; - } - }], - glusterfsscan => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], - { node => $nodename }, sub { - my $res = shift; - - foreach my $rec (@$res) { - printf "%s\n", $rec->{volname}; - } - }], - iscsiscan => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['server'], - { node => $nodename }, sub { - my $res = shift; - - my $maxlen = 0; - foreach my $rec (@$res) { - my $len = length ($rec->{target}); - $maxlen = $len if $len > $maxlen; - } - foreach my $rec (@$res) { - printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal}; - } - }], - lvmscan => [ "PVE::API2::Storage::Scan", 'lvmscan', [], - { node => $nodename }, sub { - my $res = shift; - foreach my $rec (@$res) { - printf "$rec->{vg}\n"; - } - }], - lvmthinscan => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], - { node => $nodename }, sub { - my $res = shift; - foreach my $rec (@$res) { - printf "$rec->{lv}\n"; - } - }], - zfsscan => [ "PVE::API2::Storage::Scan", 'zfsscan', [], - { node => $nodename }, sub { - my $res = shift; - - foreach my $rec (@$res) { - printf "$rec->{pool}\n"; - } - }], + scan => { + nfs => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'], { node => $nodename }, sub { + my $res = shift; + + my $maxlen = 0; + foreach my $rec (@$res) { + my $len = length ($rec->{path}); + $maxlen = $len if $len > $maxlen; + } + foreach my $rec (@$res) { + printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options}; + } + }], + cifs => [ "PVE::API2::Storage::Scan", 'cifsscan', ['server'], { node => $nodename }, sub { + my $res = shift; + + my $maxlen = 0; + foreach my $rec (@$res) { + my $len = length ($rec->{share}); + $maxlen = $len if $len > $maxlen; + } + foreach my $rec (@$res) { + printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description}; + } + }], + glusterfs => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], { node => $nodename }, sub { + my $res = shift; + + foreach my $rec (@$res) { + printf "%s\n", $rec->{volname}; + } + }], + iscsi => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['portal'], { node => $nodename }, sub { + my $res = shift; + + my $maxlen = 0; + foreach my $rec (@$res) { + my $len = length ($rec->{target}); + $maxlen = $len if $len > $maxlen; + } + foreach my $rec (@$res) { + printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal}; + } + }], + lvm => [ "PVE::API2::Storage::Scan", 'lvmscan', [], { node => $nodename }, sub { + my $res = shift; + foreach my $rec (@$res) { + printf "$rec->{vg}\n"; + } + }], + lvmthin => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], { node => $nodename }, sub { + my $res = shift; + foreach my $rec (@$res) { + printf "$rec->{lv}\n"; + } + }], + zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', [], { node => $nodename }, sub { + my $res = shift; + + foreach my $rec (@$res) { + printf "$rec->{pool}\n"; + } + }], + }, + nfsscan => { alias => 'scan nfs' }, + cifsscan => { alias => 'scan cifs' }, + glusterfsscan => { alias => 'scan glusterfs' }, + iscsiscan => { alias => 'scan iscsi' }, + lvmscan => { alias => 'scan lvm' }, + lvmthinscan => { alias => 'scan lvmthin' }, + zfsscan => { alias => 'scan zfs' }, path => [ __PACKAGE__, 'path', ['volume']], extractconfig => [__PACKAGE__, 'extractconfig', ['volume']], + export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']], + import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub { + my $volid = shift; + print PVE::Storage::volume_imported_message($volid); + }], + apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub { + my $res = shift; + + print "APIVER $res->{apiver}\n"; + print "APIAGE $res->{apiage}\n"; + }], + 'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => $nodename }, sub { + my $res = shift; + + my ($dryrun, $list) = ($res->{dryrun}, $res->{list}); + + return if !$dryrun; + + if (!scalar(@{$list})) { + print "No backups found\n"; + return; + } + + print "NOTE: this is only a preview and might not be what a subsequent\n" . + "prune call does if backups are removed/added in the meantime.\n\n"; + + my @sorted = sort { + my $vmcmp = PVE::Tools::safe_compare($a->{vmid}, $b->{vmid}, sub { $_[0] <=> $_[1] }); + return $vmcmp if $vmcmp ne 0; + return $a->{ctime} <=> $b->{ctime}; + } @{$list}; + + my $maxlen = 0; + foreach my $backup (@sorted) { + my $volid = $backup->{volid}; + $maxlen = length($volid) if length($volid) > $maxlen; + } + $maxlen+=1; + + printf("%-${maxlen}s %15s %10s\n", 'Backup', 'Backup-ID', 'Prune-Mark'); + foreach my $backup (@sorted) { + my $type = $backup->{type}; + my $vmid = $backup->{vmid}; + my $backup_id = defined($vmid) ? "$type/$vmid" : "$type"; + printf("%-${maxlen}s %15s %10s\n", $backup->{volid}, $backup_id, $backup->{mark}); + } + }], }; 1;