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