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