]> git.proxmox.com Git - pve-storage.git/blob - PVE/CLI/pvesm.pm
bump version to 8.2.1
[pve-storage.git] / PVE / CLI / pvesm.pm
1 package PVE::CLI::pvesm;
2
3 use strict;
4 use warnings;
5
6 use POSIX qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC);
7 use Fcntl ':flock';
8 use File::Path;
9 use MIME::Base64 qw(encode_base64);
10
11 use IO::Socket::IP;
12 use IO::Socket::UNIX;
13 use Socket qw(SOCK_STREAM);
14
15 use PVE::SafeSyslog;
16 use PVE::Cluster;
17 use PVE::INotify;
18 use PVE::RPCEnvironment;
19 use PVE::Storage;
20 use PVE::Tools qw(extract_param);
21 use PVE::API2::Storage::Config;
22 use PVE::API2::Storage::Content;
23 use PVE::API2::Storage::PruneBackups;
24 use PVE::API2::Storage::Scan;
25 use PVE::API2::Storage::Status;
26 use PVE::JSONSchema qw(get_standard_option);
27 use PVE::PTY;
28
29 use PVE::CLIHandler;
30
31 use base qw(PVE::CLIHandler);
32
33 my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs', 'btrfs'];
34
35 my $nodename = PVE::INotify::nodename();
36
37 sub param_mapping {
38 my ($name) = @_;
39
40 my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', {
41 func => sub {
42 my ($value) = @_;
43 return $value if $value;
44 return PVE::PTY::read_password("Enter Password: ");
45 },
46 });
47
48 my $enc_key_map = {
49 name => 'encryption-key',
50 desc => 'a file containing an encryption key, or the special value "autogen"',
51 func => sub {
52 my ($value) = @_;
53 return $value if $value eq 'autogen';
54 return PVE::Tools::file_get_contents($value);
55 }
56 };
57
58 my $master_key_map = {
59 name => 'master-pubkey',
60 desc => 'a file containing a PEM-formatted master public key',
61 func => sub {
62 my ($value) = @_;
63 return encode_base64(PVE::Tools::file_get_contents($value), '');
64 }
65 };
66
67 my $keyring_map = {
68 name => 'keyring',
69 desc => 'file containing the keyring to authenticate in the Ceph cluster',
70 func => sub {
71 my ($value) = @_;
72 return PVE::Tools::file_get_contents($value);
73 },
74 };
75
76 my $mapping = {
77 'cifsscan' => [ $password_map ],
78 'cifs' => [ $password_map ],
79 'pbs' => [ $password_map ],
80 'create' => [ $password_map, $enc_key_map, $master_key_map, $keyring_map ],
81 'update' => [ $password_map, $enc_key_map, $master_key_map, $keyring_map ],
82 };
83 return $mapping->{$name};
84 }
85
86 sub setup_environment {
87 PVE::RPCEnvironment->setup_default_cli_env();
88 }
89
90 __PACKAGE__->register_method ({
91 name => 'apiinfo',
92 path => 'apiinfo',
93 method => 'GET',
94 description => "Returns APIVER and APIAGE.",
95 parameters => {
96 additionalProperties => 0,
97 properties => {},
98 },
99 returns => {
100 type => 'object',
101 properties => {
102 apiver => { type => 'integer' },
103 apiage => { type => 'integer' },
104 },
105 },
106 code => sub {
107 return {
108 apiver => PVE::Storage::APIVER,
109 apiage => PVE::Storage::APIAGE,
110 };
111 }
112 });
113
114 __PACKAGE__->register_method ({
115 name => 'path',
116 path => 'path',
117 method => 'GET',
118 description => "Get filesystem path for specified volume",
119 parameters => {
120 additionalProperties => 0,
121 properties => {
122 volume => {
123 description => "Volume identifier",
124 type => 'string', format => 'pve-volume-id',
125 completion => \&PVE::Storage::complete_volume,
126 },
127 },
128 },
129 returns => { type => 'null' },
130
131 code => sub {
132 my ($param) = @_;
133
134 my $cfg = PVE::Storage::config();
135
136 my $path = PVE::Storage::path ($cfg, $param->{volume});
137
138 print "$path\n";
139
140 return undef;
141
142 }});
143
144 __PACKAGE__->register_method ({
145 name => 'extractconfig',
146 path => 'extractconfig',
147 method => 'GET',
148 description => "Extract configuration from vzdump backup archive.",
149 permissions => {
150 description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
151 user => 'all',
152 },
153 protected => 1,
154 parameters => {
155 additionalProperties => 0,
156 properties => {
157 volume => {
158 description => "Volume identifier",
159 type => 'string',
160 completion => \&PVE::Storage::complete_volume,
161 },
162 },
163 },
164 returns => { type => 'null' },
165 code => sub {
166 my ($param) = @_;
167 my $volume = $param->{volume};
168
169 my $rpcenv = PVE::RPCEnvironment::get();
170 my $authuser = $rpcenv->get_user();
171
172 my $storage_cfg = PVE::Storage::config();
173 PVE::Storage::check_volume_access($rpcenv, $authuser, $storage_cfg, undef, $volume);
174
175 my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
176
177 print "$config_raw\n";
178 return;
179 }});
180
181 my $print_content = sub {
182 my ($list) = @_;
183
184 my ($maxlenname, $maxsize) = (0, 0);
185 foreach my $info (@$list) {
186 my $volid = $info->{volid};
187 my $sidlen = length ($volid);
188 $maxlenname = $sidlen if $sidlen > $maxlenname;
189 $maxsize = $info->{size} if ($info->{size} // 0) > $maxsize;
190 }
191 my $sizemaxdigits = length($maxsize);
192
193 my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s";
194 printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID";
195
196 foreach my $info (@$list) {
197 next if !$info->{vmid};
198 my $volid = $info->{volid};
199
200 printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid};
201 }
202
203 foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
204 next if $info->{vmid};
205 my $volid = $info->{volid};
206
207 printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $info->{size};
208 }
209 };
210
211 my $print_status = sub {
212 my $res = shift;
213
214 my $maxlen = 0;
215 foreach my $res (@$res) {
216 my $storeid = $res->{storage};
217 $maxlen = length ($storeid) if length ($storeid) > $maxlen;
218 }
219 $maxlen+=1;
220
221 printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type',
222 'Status', 'Total', 'Used', 'Available', '%';
223
224 foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
225 my $storeid = $res->{storage};
226
227 my $active = $res->{active} ? 'active' : 'inactive';
228 my ($per, $per_fmt) = (0, '% 7.2f%%');
229 $per = ($res->{used}*100)/$res->{total} if $res->{total} > 0;
230
231 if (!$res->{enabled}) {
232 $per = 'N/A';
233 $per_fmt = '% 8s';
234 $active = 'disabled';
235 }
236
237 printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid,
238 $res->{type}, $active, $res->{total}/1024, $res->{used}/1024,
239 $res->{avail}/1024, $per;
240 }
241 };
242
243 __PACKAGE__->register_method ({
244 name => 'export',
245 path => 'export',
246 method => 'GET',
247 description => "Used internally to export a volume.",
248 protected => 1,
249 parameters => {
250 additionalProperties => 0,
251 properties => {
252 volume => {
253 description => "Volume identifier",
254 type => 'string',
255 completion => \&PVE::Storage::complete_volume,
256 },
257 format => {
258 description => "Export stream format",
259 type => 'string',
260 enum => $KNOWN_EXPORT_FORMATS,
261 },
262 filename => {
263 description => "Destination file name",
264 type => 'string',
265 },
266 base => {
267 description => "Snapshot to start an incremental stream from",
268 type => 'string',
269 pattern => qr/[a-z0-9_\-]{1,40}/i,
270 maxLength => 40,
271 optional => 1,
272 },
273 snapshot => {
274 description => "Snapshot to export",
275 type => 'string',
276 pattern => qr/[a-z0-9_\-]{1,40}/i,
277 maxLength => 40,
278 optional => 1,
279 },
280 'with-snapshots' => {
281 description =>
282 "Whether to include intermediate snapshots in the stream",
283 type => 'boolean',
284 optional => 1,
285 default => 0,
286 },
287 'snapshot-list' => {
288 description => "Ordered list of snapshots to transfer",
289 type => 'string',
290 format => 'string-list',
291 optional => 1,
292 },
293 },
294 },
295 returns => { type => 'null' },
296 code => sub {
297 my ($param) = @_;
298
299 my $with_snapshots = $param->{'with-snapshots'};
300 if (defined(my $list = $param->{'snapshot-list'})) {
301 $with_snapshots = PVE::Tools::split_list($list);
302 }
303
304 my $filename = $param->{filename};
305
306 my $outfh;
307 if ($filename eq '-') {
308 $outfh = \*STDOUT;
309 } else {
310 sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC)
311 or die "open($filename): $!\n";
312 }
313
314 eval {
315 my $cfg = PVE::Storage::config();
316 PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format},
317 $param->{snapshot}, $param->{base}, $with_snapshots);
318 };
319 my $err = $@;
320 if ($filename ne '-') {
321 close($outfh);
322 unlink($filename) if $err;
323 }
324 die $err if $err;
325 return;
326 }
327 });
328
329 __PACKAGE__->register_method ({
330 name => 'import',
331 path => 'import',
332 method => 'PUT',
333 description => "Used internally to import a volume.",
334 protected => 1,
335 parameters => {
336 additionalProperties => 0,
337 properties => {
338 volume => {
339 description => "Volume identifier",
340 type => 'string',
341 completion => \&PVE::Storage::complete_volume,
342 },
343 format => {
344 description => "Import stream format",
345 type => 'string',
346 enum => $KNOWN_EXPORT_FORMATS,
347 },
348 filename => {
349 description => "Source file name. For '-' stdin is used, the " .
350 "tcp://<IP-or-CIDR> format allows to use a TCP connection, " .
351 "the unix://PATH-TO-SOCKET format a UNIX socket as input." .
352 "Else, the file is treated as common file.",
353 type => 'string',
354 },
355 base => {
356 description => "Base snapshot of an incremental stream",
357 type => 'string',
358 pattern => qr/[a-z0-9_\-]{1,40}/i,
359 maxLength => 40,
360 optional => 1,
361 },
362 'with-snapshots' => {
363 description =>
364 "Whether the stream includes intermediate snapshots",
365 type => 'boolean',
366 optional => 1,
367 default => 0,
368 },
369 'delete-snapshot' => {
370 description => "A snapshot to delete on success",
371 type => 'string',
372 pattern => qr/[a-z0-9_\-]{1,80}/i,
373 maxLength => 80,
374 optional => 1,
375 },
376 'allow-rename' => {
377 description => "Choose a new volume ID if the requested " .
378 "volume ID already exists, instead of throwing an error.",
379 type => 'boolean',
380 optional => 1,
381 default => 0,
382 },
383 snapshot => {
384 description => "The current-state snapshot if the stream contains snapshots",
385 type => 'string',
386 pattern => qr/[a-z0-9_\-]{1,40}/i,
387 maxLength => 40,
388 optional => 1,
389 },
390 },
391 },
392 returns => { type => 'string' },
393 code => sub {
394 my ($param) = @_;
395
396 my $filename = $param->{filename};
397
398 my $infh;
399 if ($filename eq '-') {
400 $infh = \*STDIN;
401 } elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) {
402 my ($cidr, $ip, $subnet) = ($1, $2, $3);
403 if ($subnet) { # got real CIDR notation, not just IP
404 my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
405 die "Unable to get any local IP address in network '$cidr'\n"
406 if scalar(@$ips) < 1;
407 die "Got multiple local IP address in network '$cidr'\n"
408 if scalar(@$ips) > 1;
409
410 $ip = $ips->[0];
411 }
412 my $family = PVE::Tools::get_host_address_family($ip);
413 my $port = PVE::Tools::next_migrate_port($family, $ip);
414
415 my $sock_params = {
416 Listen => 1,
417 ReuseAddr => 1,
418 Proto => &Socket::IPPROTO_TCP,
419 GetAddrInfoFlags => 0,
420 LocalAddr => $ip,
421 LocalPort => $port,
422 };
423 my $socket = IO::Socket::IP->new(%$sock_params)
424 or die "failed to open socket: $!\n";
425
426 print "$ip\n$port\n"; # tell remote where to connect
427 *STDOUT->flush();
428
429 my $prev_alarm = alarm 0;
430 local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
431 alarm 30;
432 my $client = $socket->accept; # Wait for a client
433 alarm $prev_alarm;
434 close($socket);
435
436 $infh = \*$client;
437 } elsif ($filename =~ m!^unix://(.*)$!) {
438 my $socket_path = $1;
439 my $socket = IO::Socket::UNIX->new(
440 Type => SOCK_STREAM(),
441 Local => $socket_path,
442 Listen => 1,
443 ) or die "failed to open socket: $!\n";
444
445 print "ready\n";
446 *STDOUT->flush();
447
448 my $prev_alarm = alarm 0;
449 local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
450 alarm 30;
451 my $client = $socket->accept; # Wait for a client
452 alarm $prev_alarm;
453 close($socket);
454
455 $infh = \*$client;
456 } else {
457 sysopen($infh, $filename, O_RDONLY)
458 or die "open($filename): $!\n";
459 }
460
461 my $cfg = PVE::Storage::config();
462 my $volume = $param->{volume};
463 my $delete = $param->{'delete-snapshot'};
464 my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
465 $param->{snapshot}, $param->{base}, $param->{'with-snapshots'},
466 $param->{'allow-rename'});
467 PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete)
468 if defined($delete);
469 return $imported_volid;
470 }
471 });
472
473 __PACKAGE__->register_method ({
474 name => 'prunebackups',
475 path => 'prunebackups',
476 method => 'GET',
477 description => "Prune backups. Only those using the standard naming scheme are considered. " .
478 "If no keep options are specified, those from the storage configuration are used.",
479 protected => 1,
480 proxyto => 'node',
481 parameters => {
482 additionalProperties => 0,
483 properties => {
484 'dry-run' => {
485 description => "Only show what would be pruned, don't delete anything.",
486 type => 'boolean',
487 optional => 1,
488 },
489 node => get_standard_option('pve-node'),
490 storage => get_standard_option('pve-storage-id', {
491 completion => \&PVE::Storage::complete_storage_enabled,
492 }),
493 %{$PVE::Storage::Plugin::prune_backups_format},
494 type => {
495 description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
496 type => 'string',
497 optional => 1,
498 enum => ['qemu', 'lxc'],
499 },
500 vmid => get_standard_option('pve-vmid', {
501 description => "Only consider backups for this guest.",
502 optional => 1,
503 completion => \&PVE::Cluster::complete_vmid,
504 }),
505 },
506 },
507 returns => {
508 type => 'object',
509 properties => {
510 dryrun => {
511 description => 'If it was a dry run or not. The list will only be defined in that case.',
512 type => 'boolean',
513 },
514 list => {
515 type => 'array',
516 items => {
517 type => 'object',
518 properties => {
519 volid => {
520 description => "Backup volume ID.",
521 type => 'string',
522 },
523 'ctime' => {
524 description => "Creation time of the backup (seconds since the UNIX epoch).",
525 type => 'integer',
526 },
527 'mark' => {
528 description => "Whether the backup would be kept or removed. For backups that don't " .
529 "use the standard naming scheme, it's 'protected'.",
530 type => 'string',
531 },
532 type => {
533 description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
534 type => 'string',
535 },
536 'vmid' => {
537 description => "The VM the backup belongs to.",
538 type => 'integer',
539 optional => 1,
540 },
541 },
542 },
543 },
544 },
545 },
546 code => sub {
547 my ($param) = @_;
548
549 my $dryrun = extract_param($param, 'dry-run') ? 1 : 0;
550
551 my $keep_opts;
552 foreach my $keep (keys %{$PVE::Storage::Plugin::prune_backups_format}) {
553 $keep_opts->{$keep} = extract_param($param, $keep) if defined($param->{$keep});
554 }
555 $param->{'prune-backups'} = PVE::JSONSchema::print_property_string(
556 $keep_opts, $PVE::Storage::Plugin::prune_backups_format) if $keep_opts;
557
558 my $list = [];
559 if ($dryrun) {
560 $list = PVE::API2::Storage::PruneBackups->dryrun($param);
561 } else {
562 PVE::API2::Storage::PruneBackups->delete($param);
563 }
564
565 return {
566 dryrun => $dryrun,
567 list => $list,
568 };
569 }});
570
571 my $print_api_result = sub {
572 my ($data, $schema, $options) = @_;
573 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
574 };
575
576 our $cmddef = {
577 add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
578 set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
579 remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
580 status => [ "PVE::API2::Storage::Status", 'index', [],
581 { node => $nodename }, $print_status ],
582 list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
583 { node => $nodename }, $print_content ],
584 alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
585 { node => $nodename }, sub {
586 my $volid = shift;
587 print "successfully created '$volid'\n";
588 }],
589 free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
590 { node => $nodename } ],
591 scan => {
592 nfs => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'], { node => $nodename }, sub {
593 my $res = shift;
594
595 my $maxlen = 0;
596 foreach my $rec (@$res) {
597 my $len = length ($rec->{path});
598 $maxlen = $len if $len > $maxlen;
599 }
600 foreach my $rec (@$res) {
601 printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
602 }
603 }],
604 cifs => [ "PVE::API2::Storage::Scan", 'cifsscan', ['server'], { node => $nodename }, sub {
605 my $res = shift;
606
607 my $maxlen = 0;
608 foreach my $rec (@$res) {
609 my $len = length ($rec->{share});
610 $maxlen = $len if $len > $maxlen;
611 }
612 foreach my $rec (@$res) {
613 printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
614 }
615 }],
616 glusterfs => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], { node => $nodename }, sub {
617 my $res = shift;
618
619 foreach my $rec (@$res) {
620 printf "%s\n", $rec->{volname};
621 }
622 }],
623 iscsi => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['portal'], { node => $nodename }, sub {
624 my $res = shift;
625
626 my $maxlen = 0;
627 foreach my $rec (@$res) {
628 my $len = length ($rec->{target});
629 $maxlen = $len if $len > $maxlen;
630 }
631 foreach my $rec (@$res) {
632 printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
633 }
634 }],
635 lvm => [ "PVE::API2::Storage::Scan", 'lvmscan', [], { node => $nodename }, sub {
636 my $res = shift;
637 foreach my $rec (@$res) {
638 printf "$rec->{vg}\n";
639 }
640 }],
641 lvmthin => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], { node => $nodename }, sub {
642 my $res = shift;
643 foreach my $rec (@$res) {
644 printf "$rec->{lv}\n";
645 }
646 }],
647 pbs => [
648 "PVE::API2::Storage::Scan",
649 'pbsscan',
650 ['server', 'username'],
651 { node => $nodename },
652 $print_api_result,
653 $PVE::RESTHandler::standard_output_options,
654 ],
655 zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', [], { node => $nodename }, sub {
656 my $res = shift;
657
658 foreach my $rec (@$res) {
659 printf "$rec->{pool}\n";
660 }
661 }],
662 },
663 nfsscan => { alias => 'scan nfs' },
664 cifsscan => { alias => 'scan cifs' },
665 glusterfsscan => { alias => 'scan glusterfs' },
666 iscsiscan => { alias => 'scan iscsi' },
667 lvmscan => { alias => 'scan lvm' },
668 lvmthinscan => { alias => 'scan lvmthin' },
669 zfsscan => { alias => 'scan zfs' },
670 path => [ __PACKAGE__, 'path', ['volume']],
671 extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
672 export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
673 import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub {
674 my $volid = shift;
675 print PVE::Storage::volume_imported_message($volid);
676 }],
677 apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub {
678 my $res = shift;
679
680 print "APIVER $res->{apiver}\n";
681 print "APIAGE $res->{apiage}\n";
682 }],
683 'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => $nodename }, sub {
684 my $res = shift;
685
686 my ($dryrun, $list) = ($res->{dryrun}, $res->{list});
687
688 return if !$dryrun;
689
690 if (!scalar(@{$list})) {
691 print "No backups found\n";
692 return;
693 }
694
695 print "NOTE: this is only a preview and might not be what a subsequent\n" .
696 "prune call does if backups are removed/added in the meantime.\n\n";
697
698 my @sorted = sort {
699 my $vmcmp = PVE::Tools::safe_compare($a->{vmid}, $b->{vmid}, sub { $_[0] <=> $_[1] });
700 return $vmcmp if $vmcmp ne 0;
701 return $a->{ctime} <=> $b->{ctime};
702 } @{$list};
703
704 my $maxlen = 0;
705 foreach my $backup (@sorted) {
706 my $volid = $backup->{volid};
707 $maxlen = length($volid) if length($volid) > $maxlen;
708 }
709 $maxlen+=1;
710
711 printf("%-${maxlen}s %15s %10s\n", 'Backup', 'Backup-ID', 'Prune-Mark');
712 foreach my $backup (@sorted) {
713 my $type = $backup->{type};
714 my $vmid = $backup->{vmid};
715 my $backup_id = defined($vmid) ? "$type/$vmid" : "$type";
716 printf("%-${maxlen}s %15s %10s\n", $backup->{volid}, $backup_id, $backup->{mark});
717 }
718 }],
719 };
720
721 1;