]> git.proxmox.com Git - pve-storage.git/blob - PVE/CLI/pvesm.pm
Introduce allow_rename parameter for pvesm import and storage_migrate
[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
10 use PVE::SafeSyslog;
11 use PVE::Cluster;
12 use PVE::INotify;
13 use PVE::RPCEnvironment;
14 use PVE::Storage;
15 use PVE::API2::Storage::Config;
16 use PVE::API2::Storage::Content;
17 use PVE::API2::Storage::Status;
18 use PVE::JSONSchema qw(get_standard_option);
19 use PVE::PTY;
20
21 use PVE::CLIHandler;
22
23 use base qw(PVE::CLIHandler);
24
25 my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs'];
26
27 my $nodename = PVE::INotify::nodename();
28
29 sub param_mapping {
30 my ($name) = @_;
31
32 my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', {
33 func => sub {
34 my ($value) = @_;
35 return $value if $value;
36 return PVE::PTY::read_password("Enter Password: ");
37 },
38 });
39 my $mapping = {
40 'cifsscan' => [ $password_map ],
41 'create' => [ $password_map ],
42 };
43 return $mapping->{$name};
44 }
45
46 sub setup_environment {
47 PVE::RPCEnvironment->setup_default_cli_env();
48 }
49
50 __PACKAGE__->register_method ({
51 name => 'apiinfo',
52 path => 'apiinfo',
53 method => 'GET',
54 description => "Returns APIVER and APIAGE.",
55 parameters => {
56 additionalProperties => 0,
57 properties => {},
58 },
59 returns => {
60 type => 'object',
61 properties => {
62 apiver => { type => 'integer' },
63 apiage => { type => 'integer' },
64 },
65 },
66 code => sub {
67 return {
68 apiver => PVE::Storage::APIVER,
69 apiage => PVE::Storage::APIAGE,
70 };
71 }
72 });
73
74 __PACKAGE__->register_method ({
75 name => 'path',
76 path => 'path',
77 method => 'GET',
78 description => "Get filesystem path for specified volume",
79 parameters => {
80 additionalProperties => 0,
81 properties => {
82 volume => {
83 description => "Volume identifier",
84 type => 'string', format => 'pve-volume-id',
85 completion => \&PVE::Storage::complete_volume,
86 },
87 },
88 },
89 returns => { type => 'null' },
90
91 code => sub {
92 my ($param) = @_;
93
94 my $cfg = PVE::Storage::config();
95
96 my $path = PVE::Storage::path ($cfg, $param->{volume});
97
98 print "$path\n";
99
100 return undef;
101
102 }});
103
104 __PACKAGE__->register_method ({
105 name => 'extractconfig',
106 path => 'extractconfig',
107 method => 'GET',
108 description => "Extract configuration from vzdump backup archive.",
109 permissions => {
110 description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
111 user => 'all',
112 },
113 protected => 1,
114 parameters => {
115 additionalProperties => 0,
116 properties => {
117 volume => {
118 description => "Volume identifier",
119 type => 'string',
120 completion => \&PVE::Storage::complete_volume,
121 },
122 },
123 },
124 returns => { type => 'null' },
125 code => sub {
126 my ($param) = @_;
127 my $volume = $param->{volume};
128
129 my $rpcenv = PVE::RPCEnvironment::get();
130 my $authuser = $rpcenv->get_user();
131
132 my $storage_cfg = PVE::Storage::config();
133 PVE::Storage::check_volume_access($rpcenv, $authuser, $storage_cfg, undef, $volume);
134
135 my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
136
137 print "$config_raw\n";
138 return;
139 }});
140
141 my $print_content = sub {
142 my ($list) = @_;
143
144 my ($maxlenname, $maxsize) = (0, 0);
145 foreach my $info (@$list) {
146 my $volid = $info->{volid};
147 my $sidlen = length ($volid);
148 $maxlenname = $sidlen if $sidlen > $maxlenname;
149 $maxsize = $info->{size} if ($info->{size} // 0) > $maxsize;
150 }
151 my $sizemaxdigits = length($maxsize);
152
153 my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s";
154 printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID";
155
156 foreach my $info (@$list) {
157 next if !$info->{vmid};
158 my $volid = $info->{volid};
159
160 printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid};
161 }
162
163 foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
164 next if $info->{vmid};
165 my $volid = $info->{volid};
166
167 printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $info->{size};
168 }
169 };
170
171 my $print_status = sub {
172 my $res = shift;
173
174 my $maxlen = 0;
175 foreach my $res (@$res) {
176 my $storeid = $res->{storage};
177 $maxlen = length ($storeid) if length ($storeid) > $maxlen;
178 }
179 $maxlen+=1;
180
181 printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type',
182 'Status', 'Total', 'Used', 'Available', '%';
183
184 foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
185 my $storeid = $res->{storage};
186
187 my $active = $res->{active} ? 'active' : 'inactive';
188 my ($per, $per_fmt) = (0, '% 7.2f%%');
189 $per = ($res->{used}*100)/$res->{total} if $res->{total} > 0;
190
191 if (!$res->{enabled}) {
192 $per = 'N/A';
193 $per_fmt = '% 8s';
194 $active = 'disabled';
195 }
196
197 printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid,
198 $res->{type}, $active, $res->{total}/1024, $res->{used}/1024,
199 $res->{avail}/1024, $per;
200 }
201 };
202
203 __PACKAGE__->register_method ({
204 name => 'export',
205 path => 'export',
206 method => 'GET',
207 description => "Export a volume.",
208 protected => 1,
209 parameters => {
210 additionalProperties => 0,
211 properties => {
212 volume => {
213 description => "Volume identifier",
214 type => 'string',
215 completion => \&PVE::Storage::complete_volume,
216 },
217 format => {
218 description => "Export stream format",
219 type => 'string',
220 enum => $KNOWN_EXPORT_FORMATS,
221 },
222 filename => {
223 description => "Destination file name",
224 type => 'string',
225 },
226 base => {
227 description => "Snapshot to start an incremental stream from",
228 type => 'string',
229 pattern => qr/[a-z0-9_\-]{1,40}/,
230 maxLength => 40,
231 optional => 1,
232 },
233 snapshot => {
234 description => "Snapshot to export",
235 type => 'string',
236 pattern => qr/[a-z0-9_\-]{1,40}/,
237 maxLength => 40,
238 optional => 1,
239 },
240 'with-snapshots' => {
241 description =>
242 "Whether to include intermediate snapshots in the stream",
243 type => 'boolean',
244 optional => 1,
245 default => 0,
246 },
247 },
248 },
249 returns => { type => 'null' },
250 code => sub {
251 my ($param) = @_;
252
253 my $filename = $param->{filename};
254
255 my $outfh;
256 if ($filename eq '-') {
257 $outfh = \*STDOUT;
258 } else {
259 sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC)
260 or die "open($filename): $!\n";
261 }
262
263 eval {
264 my $cfg = PVE::Storage::config();
265 PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format},
266 $param->{snapshot}, $param->{base}, $param->{'with-snapshots'});
267 };
268 my $err = $@;
269 if ($filename ne '-') {
270 close($outfh);
271 unlink($filename) if $err;
272 }
273 die $err if $err;
274 return;
275 }
276 });
277
278 __PACKAGE__->register_method ({
279 name => 'import',
280 path => 'import',
281 method => 'PUT',
282 description => "Import a volume.",
283 protected => 1,
284 parameters => {
285 additionalProperties => 0,
286 properties => {
287 volume => {
288 description => "Volume identifier",
289 type => 'string',
290 completion => \&PVE::Storage::complete_volume,
291 },
292 format => {
293 description => "Import stream format",
294 type => 'string',
295 enum => $KNOWN_EXPORT_FORMATS,
296 },
297 filename => {
298 description => "Source file name. For '-' stdin is used, the " .
299 "tcp://<IP-or-CIDR> format allows to use a TCP connection as input. " .
300 "Else, the file is treated as common file.",
301 type => 'string',
302 },
303 base => {
304 description => "Base snapshot of an incremental stream",
305 type => 'string',
306 pattern => qr/[a-z0-9_\-]{1,40}/,
307 maxLength => 40,
308 optional => 1,
309 },
310 'with-snapshots' => {
311 description =>
312 "Whether the stream includes intermediate snapshots",
313 type => 'boolean',
314 optional => 1,
315 default => 0,
316 },
317 'delete-snapshot' => {
318 description => "A snapshot to delete on success",
319 type => 'string',
320 pattern => qr/[a-z0-9_\-]{1,80}/,
321 maxLength => 80,
322 optional => 1,
323 },
324 'allow-rename' => {
325 description => "Choose a new volume ID if the requested " .
326 "volume ID already exists, instead of throwing an error.",
327 type => 'boolean',
328 optional => 1,
329 default => 0,
330 },
331 },
332 },
333 returns => { type => 'string' },
334 code => sub {
335 my ($param) = @_;
336
337 my $filename = $param->{filename};
338
339 my $infh;
340 if ($filename eq '-') {
341 $infh = \*STDIN;
342 } elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) {
343 my ($cidr, $ip, $subnet) = ($1, $2, $3);
344 if ($subnet) { # got real CIDR notation, not just IP
345 my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
346 die "Unable to get any local IP address in network '$cidr'\n"
347 if scalar(@$ips) < 1;
348 die "Got multiple local IP address in network '$cidr'\n"
349 if scalar(@$ips) > 1;
350
351 $ip = $ips->[0];
352 }
353 my $family = PVE::Tools::get_host_address_family($ip);
354 my $port = PVE::Tools::next_migrate_port($family, $ip);
355
356 my $sock_params = {
357 Listen => 1,
358 ReuseAddr => 1,
359 Proto => &Socket::IPPROTO_TCP,
360 GetAddrInfoFlags => 0,
361 LocalAddr => $ip,
362 LocalPort => $port,
363 };
364 my $socket = IO::Socket::IP->new(%$sock_params)
365 or die "failed to open socket: $!\n";
366
367 print "$ip\n$port\n"; # tell remote where to connect
368 *STDOUT->flush();
369
370 my $prev_alarm = alarm 0;
371 local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
372 alarm 30;
373 my $client = $socket->accept; # Wait for a client
374 alarm $prev_alarm;
375 close($socket);
376
377 $infh = \*$client;
378 } else {
379 sysopen($infh, $filename, O_RDONLY)
380 or die "open($filename): $!\n";
381 }
382
383 my $cfg = PVE::Storage::config();
384 my $volume = $param->{volume};
385 my $delete = $param->{'delete-snapshot'};
386 my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
387 $param->{base}, $param->{'with-snapshots'}, $param->{'allow-rename'});
388 PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete)
389 if defined($delete);
390 return $imported_volid;
391 }
392 });
393
394 __PACKAGE__->register_method ({
395 name => 'nfsscan',
396 path => 'nfs',
397 method => 'GET',
398 description => "Scan remote NFS server.",
399 protected => 1,
400 proxyto => "node",
401 permissions => {
402 check => ['perm', '/storage', ['Datastore.Allocate']],
403 },
404 parameters => {
405 additionalProperties => 0,
406 properties => {
407 node => get_standard_option('pve-node'),
408 server => {
409 description => "The server address (name or IP).",
410 type => 'string', format => 'pve-storage-server',
411 },
412 },
413 },
414 returns => {
415 type => 'array',
416 items => {
417 type => "object",
418 properties => {
419 path => {
420 description => "The exported path.",
421 type => 'string',
422 },
423 options => {
424 description => "NFS export options.",
425 type => 'string',
426 },
427 },
428 },
429 },
430 code => sub {
431 my ($param) = @_;
432
433 my $server = $param->{server};
434 my $res = PVE::Storage::scan_nfs($server);
435
436 my $data = [];
437 foreach my $k (keys %$res) {
438 push @$data, { path => $k, options => $res->{$k} };
439 }
440 return $data;
441 }});
442
443 __PACKAGE__->register_method ({
444 name => 'cifsscan',
445 path => 'cifs',
446 method => 'GET',
447 description => "Scan remote CIFS server.",
448 protected => 1,
449 proxyto => "node",
450 permissions => {
451 check => ['perm', '/storage', ['Datastore.Allocate']],
452 },
453 parameters => {
454 additionalProperties => 0,
455 properties => {
456 node => get_standard_option('pve-node'),
457 server => {
458 description => "The server address (name or IP).",
459 type => 'string', format => 'pve-storage-server',
460 },
461 username => {
462 description => "User name.",
463 type => 'string',
464 optional => 1,
465 },
466 password => {
467 description => "User password.",
468 type => 'string',
469 optional => 1,
470 },
471 domain => {
472 description => "SMB domain (Workgroup).",
473 type => 'string',
474 optional => 1,
475 },
476 },
477 },
478 returns => {
479 type => 'array',
480 items => {
481 type => "object",
482 properties => {
483 share => {
484 description => "The cifs share name.",
485 type => 'string',
486 },
487 description => {
488 description => "Descriptive text from server.",
489 type => 'string',
490 },
491 },
492 },
493 },
494 code => sub {
495 my ($param) = @_;
496
497 my $server = $param->{server};
498
499 my $username = $param->{username};
500 my $password = $param->{password};
501 my $domain = $param->{domain};
502
503 my $res = PVE::Storage::scan_cifs($server, $username, $password, $domain);
504
505 my $data = [];
506 foreach my $k (keys %$res) {
507 next if $k =~ m/NT_STATUS_/;
508 push @$data, { share => $k, description => $res->{$k} };
509 }
510
511 return $data;
512 }});
513
514 # Note: GlusterFS currently does not have an equivalent of showmount.
515 # As workaround, we simply use nfs showmount.
516 # see http://www.gluster.org/category/volumes/
517
518 __PACKAGE__->register_method ({
519 name => 'glusterfsscan',
520 path => 'glusterfs',
521 method => 'GET',
522 description => "Scan remote GlusterFS server.",
523 protected => 1,
524 proxyto => "node",
525 permissions => {
526 check => ['perm', '/storage', ['Datastore.Allocate']],
527 },
528 parameters => {
529 additionalProperties => 0,
530 properties => {
531 node => get_standard_option('pve-node'),
532 server => {
533 description => "The server address (name or IP).",
534 type => 'string', format => 'pve-storage-server',
535 },
536 },
537 },
538 returns => {
539 type => 'array',
540 items => {
541 type => "object",
542 properties => {
543 volname => {
544 description => "The volume name.",
545 type => 'string',
546 },
547 },
548 },
549 },
550 code => sub {
551 my ($param) = @_;
552
553 my $server = $param->{server};
554 my $res = PVE::Storage::scan_nfs($server);
555
556 my $data = [];
557 foreach my $path (keys %$res) {
558 if ($path =~ m!^/([^\s/]+)$!) {
559 push @$data, { volname => $1 };
560 }
561 }
562 return $data;
563 }});
564
565 __PACKAGE__->register_method ({
566 name => 'iscsiscan',
567 path => 'iscsi',
568 method => 'GET',
569 description => "Scan remote iSCSI server.",
570 protected => 1,
571 proxyto => "node",
572 permissions => {
573 check => ['perm', '/storage', ['Datastore.Allocate']],
574 },
575 parameters => {
576 additionalProperties => 0,
577 properties => {
578 node => get_standard_option('pve-node'),
579 portal => {
580 description => "The iSCSI portal (IP or DNS name with optional port).",
581 type => 'string', format => 'pve-storage-portal-dns',
582 },
583 },
584 },
585 returns => {
586 type => 'array',
587 items => {
588 type => "object",
589 properties => {
590 target => {
591 description => "The iSCSI target name.",
592 type => 'string',
593 },
594 portal => {
595 description => "The iSCSI portal name.",
596 type => 'string',
597 },
598 },
599 },
600 },
601 code => sub {
602 my ($param) = @_;
603
604 my $res = PVE::Storage::scan_iscsi($param->{portal});
605
606 my $data = [];
607 foreach my $k (keys %$res) {
608 push @$data, { target => $k, portal => join(',', @{$res->{$k}}) };
609 }
610
611 return $data;
612 }});
613
614 __PACKAGE__->register_method ({
615 name => 'lvmscan',
616 path => 'lvm',
617 method => 'GET',
618 description => "List local LVM volume groups.",
619 protected => 1,
620 proxyto => "node",
621 permissions => {
622 check => ['perm', '/storage', ['Datastore.Allocate']],
623 },
624 parameters => {
625 additionalProperties => 0,
626 properties => {
627 node => get_standard_option('pve-node'),
628 },
629 },
630 returns => {
631 type => 'array',
632 items => {
633 type => "object",
634 properties => {
635 vg => {
636 description => "The LVM logical volume group name.",
637 type => 'string',
638 },
639 },
640 },
641 },
642 code => sub {
643 my ($param) = @_;
644
645 my $res = PVE::Storage::LVMPlugin::lvm_vgs();
646 return PVE::RESTHandler::hash_to_array($res, 'vg');
647 }});
648
649 __PACKAGE__->register_method ({
650 name => 'lvmthinscan',
651 path => 'lvmthin',
652 method => 'GET',
653 description => "List local LVM Thin Pools.",
654 protected => 1,
655 proxyto => "node",
656 permissions => {
657 check => ['perm', '/storage', ['Datastore.Allocate']],
658 },
659 parameters => {
660 additionalProperties => 0,
661 properties => {
662 node => get_standard_option('pve-node'),
663 vg => {
664 type => 'string',
665 pattern => '[a-zA-Z0-9\.\+\_][a-zA-Z0-9\.\+\_\-]+', # see lvm(8) manpage
666 maxLength => 100,
667 },
668 },
669 },
670 returns => {
671 type => 'array',
672 items => {
673 type => "object",
674 properties => {
675 lv => {
676 description => "The LVM Thin Pool name (LVM logical volume).",
677 type => 'string',
678 },
679 },
680 },
681 },
682 code => sub {
683 my ($param) = @_;
684
685 return PVE::Storage::LvmThinPlugin::list_thinpools($param->{vg});
686 }});
687
688 __PACKAGE__->register_method ({
689 name => 'zfsscan',
690 path => 'zfs',
691 method => 'GET',
692 description => "Scan zfs pool list on local node.",
693 protected => 1,
694 proxyto => "node",
695 permissions => {
696 check => ['perm', '/storage', ['Datastore.Allocate']],
697 },
698 parameters => {
699 additionalProperties => 0,
700 properties => {
701 node => get_standard_option('pve-node'),
702 },
703 },
704 returns => {
705 type => 'array',
706 items => {
707 type => "object",
708 properties => {
709 pool => {
710 description => "ZFS pool name.",
711 type => 'string',
712 },
713 },
714 },
715 },
716 code => sub {
717 my ($param) = @_;
718
719 return PVE::Storage::scan_zfs();
720 }});
721
722 our $cmddef = {
723 add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
724 set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
725 remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
726 status => [ "PVE::API2::Storage::Status", 'index', [],
727 { node => $nodename }, $print_status ],
728 list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
729 { node => $nodename }, $print_content ],
730 alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
731 { node => $nodename }, sub {
732 my $volid = shift;
733 print "successfully created '$volid'\n";
734 }],
735 free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
736 { node => $nodename } ],
737 scan => {
738 nfs => [ __PACKAGE__, 'nfsscan', ['server'], { node => $nodename }, sub {
739 my $res = shift;
740
741 my $maxlen = 0;
742 foreach my $rec (@$res) {
743 my $len = length ($rec->{path});
744 $maxlen = $len if $len > $maxlen;
745 }
746 foreach my $rec (@$res) {
747 printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
748 }
749 }],
750 cifs => [ __PACKAGE__, 'cifsscan', ['server'], { node => $nodename }, sub {
751 my $res = shift;
752
753 my $maxlen = 0;
754 foreach my $rec (@$res) {
755 my $len = length ($rec->{share});
756 $maxlen = $len if $len > $maxlen;
757 }
758 foreach my $rec (@$res) {
759 printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
760 }
761 }],
762 glusterfs => [ __PACKAGE__, 'glusterfsscan', ['server'], { node => $nodename }, sub {
763 my $res = shift;
764
765 foreach my $rec (@$res) {
766 printf "%s\n", $rec->{volname};
767 }
768 }],
769 iscsi => [ __PACKAGE__, 'iscsiscan', ['portal'], { node => $nodename }, sub {
770 my $res = shift;
771
772 my $maxlen = 0;
773 foreach my $rec (@$res) {
774 my $len = length ($rec->{target});
775 $maxlen = $len if $len > $maxlen;
776 }
777 foreach my $rec (@$res) {
778 printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
779 }
780 }],
781 lvm => [ __PACKAGE__, 'lvmscan', [], { node => $nodename }, sub {
782 my $res = shift;
783 foreach my $rec (@$res) {
784 printf "$rec->{vg}\n";
785 }
786 }],
787 lvmthin => [ __PACKAGE__, 'lvmthinscan', ['vg'], { node => $nodename }, sub {
788 my $res = shift;
789 foreach my $rec (@$res) {
790 printf "$rec->{lv}\n";
791 }
792 }],
793 zfs => [ __PACKAGE__, 'zfsscan', [], { node => $nodename }, sub {
794 my $res = shift;
795
796 foreach my $rec (@$res) {
797 printf "$rec->{pool}\n";
798 }
799 }],
800 },
801 nfsscan => { alias => 'scan nfs' },
802 cifsscan => { alias => 'scan cifs' },
803 glusterfsscan => { alias => 'scan glusterfs' },
804 iscsiscan => { alias => 'scan iscsi' },
805 lvmscan => { alias => 'scan lvm' },
806 lvmthinscan => { alias => 'scan lvmthin' },
807 zfsscan => { alias => 'scan zfs' },
808 path => [ __PACKAGE__, 'path', ['volume']],
809 extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
810 export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
811 import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub {
812 my $volid = shift;
813 print PVE::Storage::volume_imported_message($volid);
814 }],
815 apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub {
816 my $res = shift;
817
818 print "APIVER $res->{apiver}\n";
819 print "APIAGE $res->{apiage}\n";
820 }],
821 };
822
823 1;