]> git.proxmox.com Git - pve-storage.git/blob - PVE/CLI/pvesm.pm
use correct local IP address helper
[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 single local IP address in network '$cidr'\n"
314 if scalar(@$ips) != 1;
315 $ip = @$ips[0];
316 }
317 my $family = PVE::Tools::get_host_address_family($ip);
318 my $port = PVE::Tools::next_migrate_port($family, $ip);
319
320 my $sock_params = {
321 Listen => 1,
322 ReuseAddr => 1,
323 Proto => &Socket::IPPROTO_TCP,
324 GetAddrInfoFlags => 0,
325 LocalAddr => $ip,
326 LocalPort => $port,
327 };
328 my $socket = IO::Socket::IP->new(%$sock_params)
329 or die "failed to open socket: $!\n";
330
331 print "$ip\n$port\n"; # tell remote where to connect
332 *STDOUT->flush();
333
334 my $prev_alarm = alarm 0;
335 local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
336 alarm 30;
337 my $client = $socket->accept; # Wait for a client
338 alarm $prev_alarm;
339 close($socket);
340
341 $infh = \*$client;
342 } else {
343 sysopen($infh, $filename, O_RDONLY)
344 or die "open($filename): $!\n";
345 }
346
347 my $cfg = PVE::Storage::config();
348 my $volume = $param->{volume};
349 my $delete = $param->{'delete-snapshot'};
350 PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
351 $param->{base}, $param->{'with-snapshots'});
352 PVE::Storage::volume_snapshot_delete($cfg, $volume, $delete)
353 if defined($delete);
354 return;
355 }
356 });
357
358 __PACKAGE__->register_method ({
359 name => 'nfsscan',
360 path => 'nfs',
361 method => 'GET',
362 description => "Scan remote NFS server.",
363 protected => 1,
364 proxyto => "node",
365 permissions => {
366 check => ['perm', '/storage', ['Datastore.Allocate']],
367 },
368 parameters => {
369 additionalProperties => 0,
370 properties => {
371 node => get_standard_option('pve-node'),
372 server => {
373 description => "The server address (name or IP).",
374 type => 'string', format => 'pve-storage-server',
375 },
376 },
377 },
378 returns => {
379 type => 'array',
380 items => {
381 type => "object",
382 properties => {
383 path => {
384 description => "The exported path.",
385 type => 'string',
386 },
387 options => {
388 description => "NFS export options.",
389 type => 'string',
390 },
391 },
392 },
393 },
394 code => sub {
395 my ($param) = @_;
396
397 my $server = $param->{server};
398 my $res = PVE::Storage::scan_nfs($server);
399
400 my $data = [];
401 foreach my $k (keys %$res) {
402 push @$data, { path => $k, options => $res->{$k} };
403 }
404 return $data;
405 }});
406
407 __PACKAGE__->register_method ({
408 name => 'cifsscan',
409 path => 'cifs',
410 method => 'GET',
411 description => "Scan remote CIFS server.",
412 protected => 1,
413 proxyto => "node",
414 permissions => {
415 check => ['perm', '/storage', ['Datastore.Allocate']],
416 },
417 parameters => {
418 additionalProperties => 0,
419 properties => {
420 node => get_standard_option('pve-node'),
421 server => {
422 description => "The server address (name or IP).",
423 type => 'string', format => 'pve-storage-server',
424 },
425 username => {
426 description => "User name.",
427 type => 'string',
428 optional => 1,
429 },
430 password => {
431 description => "User password.",
432 type => 'string',
433 optional => 1,
434 },
435 domain => {
436 description => "SMB domain (Workgroup).",
437 type => 'string',
438 optional => 1,
439 },
440 },
441 },
442 returns => {
443 type => 'array',
444 items => {
445 type => "object",
446 properties => {
447 share => {
448 description => "The cifs share name.",
449 type => 'string',
450 },
451 description => {
452 description => "Descriptive text from server.",
453 type => 'string',
454 },
455 },
456 },
457 },
458 code => sub {
459 my ($param) = @_;
460
461 my $server = $param->{server};
462
463 my $username = $param->{username};
464 my $password = $param->{password};
465 my $domain = $param->{domain};
466
467 my $res = PVE::Storage::scan_cifs($server, $username, $password, $domain);
468
469 my $data = [];
470 foreach my $k (keys %$res) {
471 next if $k =~ m/NT_STATUS_/;
472 push @$data, { share => $k, description => $res->{$k} };
473 }
474
475 return $data;
476 }});
477
478 # Note: GlusterFS currently does not have an equivalent of showmount.
479 # As workaround, we simply use nfs showmount.
480 # see http://www.gluster.org/category/volumes/
481
482 __PACKAGE__->register_method ({
483 name => 'glusterfsscan',
484 path => 'glusterfs',
485 method => 'GET',
486 description => "Scan remote GlusterFS server.",
487 protected => 1,
488 proxyto => "node",
489 permissions => {
490 check => ['perm', '/storage', ['Datastore.Allocate']],
491 },
492 parameters => {
493 additionalProperties => 0,
494 properties => {
495 node => get_standard_option('pve-node'),
496 server => {
497 description => "The server address (name or IP).",
498 type => 'string', format => 'pve-storage-server',
499 },
500 },
501 },
502 returns => {
503 type => 'array',
504 items => {
505 type => "object",
506 properties => {
507 volname => {
508 description => "The volume name.",
509 type => 'string',
510 },
511 },
512 },
513 },
514 code => sub {
515 my ($param) = @_;
516
517 my $server = $param->{server};
518 my $res = PVE::Storage::scan_nfs($server);
519
520 my $data = [];
521 foreach my $path (keys %$res) {
522 if ($path =~ m!^/([^\s/]+)$!) {
523 push @$data, { volname => $1 };
524 }
525 }
526 return $data;
527 }});
528
529 __PACKAGE__->register_method ({
530 name => 'iscsiscan',
531 path => 'iscsi',
532 method => 'GET',
533 description => "Scan remote iSCSI server.",
534 protected => 1,
535 proxyto => "node",
536 permissions => {
537 check => ['perm', '/storage', ['Datastore.Allocate']],
538 },
539 parameters => {
540 additionalProperties => 0,
541 properties => {
542 node => get_standard_option('pve-node'),
543 portal => {
544 description => "The iSCSI portal (IP or DNS name with optional port).",
545 type => 'string', format => 'pve-storage-portal-dns',
546 },
547 },
548 },
549 returns => {
550 type => 'array',
551 items => {
552 type => "object",
553 properties => {
554 target => {
555 description => "The iSCSI target name.",
556 type => 'string',
557 },
558 portal => {
559 description => "The iSCSI portal name.",
560 type => 'string',
561 },
562 },
563 },
564 },
565 code => sub {
566 my ($param) = @_;
567
568 my $res = PVE::Storage::scan_iscsi($param->{portal});
569
570 my $data = [];
571 foreach my $k (keys %$res) {
572 push @$data, { target => $k, portal => join(',', @{$res->{$k}}) };
573 }
574
575 return $data;
576 }});
577
578 __PACKAGE__->register_method ({
579 name => 'lvmscan',
580 path => 'lvm',
581 method => 'GET',
582 description => "List local LVM volume groups.",
583 protected => 1,
584 proxyto => "node",
585 permissions => {
586 check => ['perm', '/storage', ['Datastore.Allocate']],
587 },
588 parameters => {
589 additionalProperties => 0,
590 properties => {
591 node => get_standard_option('pve-node'),
592 },
593 },
594 returns => {
595 type => 'array',
596 items => {
597 type => "object",
598 properties => {
599 vg => {
600 description => "The LVM logical volume group name.",
601 type => 'string',
602 },
603 },
604 },
605 },
606 code => sub {
607 my ($param) = @_;
608
609 my $res = PVE::Storage::LVMPlugin::lvm_vgs();
610 return PVE::RESTHandler::hash_to_array($res, 'vg');
611 }});
612
613 __PACKAGE__->register_method ({
614 name => 'lvmthinscan',
615 path => 'lvmthin',
616 method => 'GET',
617 description => "List local LVM Thin Pools.",
618 protected => 1,
619 proxyto => "node",
620 permissions => {
621 check => ['perm', '/storage', ['Datastore.Allocate']],
622 },
623 parameters => {
624 additionalProperties => 0,
625 properties => {
626 node => get_standard_option('pve-node'),
627 vg => {
628 type => 'string',
629 pattern => '[a-zA-Z0-9\.\+\_][a-zA-Z0-9\.\+\_\-]+', # see lvm(8) manpage
630 maxLength => 100,
631 },
632 },
633 },
634 returns => {
635 type => 'array',
636 items => {
637 type => "object",
638 properties => {
639 lv => {
640 description => "The LVM Thin Pool name (LVM logical volume).",
641 type => 'string',
642 },
643 },
644 },
645 },
646 code => sub {
647 my ($param) = @_;
648
649 return PVE::Storage::LvmThinPlugin::list_thinpools($param->{vg});
650 }});
651
652 __PACKAGE__->register_method ({
653 name => 'zfsscan',
654 path => 'zfs',
655 method => 'GET',
656 description => "Scan zfs pool list on local node.",
657 protected => 1,
658 proxyto => "node",
659 permissions => {
660 check => ['perm', '/storage', ['Datastore.Allocate']],
661 },
662 parameters => {
663 additionalProperties => 0,
664 properties => {
665 node => get_standard_option('pve-node'),
666 },
667 },
668 returns => {
669 type => 'array',
670 items => {
671 type => "object",
672 properties => {
673 pool => {
674 description => "ZFS pool name.",
675 type => 'string',
676 },
677 },
678 },
679 },
680 code => sub {
681 my ($param) = @_;
682
683 return PVE::Storage::scan_zfs();
684 }});
685
686 our $cmddef = {
687 add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
688 set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
689 remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
690 status => [ "PVE::API2::Storage::Status", 'index', [],
691 { node => $nodename }, $print_status ],
692 list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
693 { node => $nodename }, $print_content ],
694 alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
695 { node => $nodename }, sub {
696 my $volid = shift;
697 print "successfully created '$volid'\n";
698 }],
699 free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
700 { node => $nodename } ],
701 scan => {
702 nfs => [ __PACKAGE__, 'nfsscan', ['server'], { node => $nodename }, sub {
703 my $res = shift;
704
705 my $maxlen = 0;
706 foreach my $rec (@$res) {
707 my $len = length ($rec->{path});
708 $maxlen = $len if $len > $maxlen;
709 }
710 foreach my $rec (@$res) {
711 printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
712 }
713 }],
714 cifs => [ __PACKAGE__, 'cifsscan', ['server'], { node => $nodename }, sub {
715 my $res = shift;
716
717 my $maxlen = 0;
718 foreach my $rec (@$res) {
719 my $len = length ($rec->{share});
720 $maxlen = $len if $len > $maxlen;
721 }
722 foreach my $rec (@$res) {
723 printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
724 }
725 }],
726 glusterfs => [ __PACKAGE__, 'glusterfsscan', ['server'], { node => $nodename }, sub {
727 my $res = shift;
728
729 foreach my $rec (@$res) {
730 printf "%s\n", $rec->{volname};
731 }
732 }],
733 iscsi => [ __PACKAGE__, 'iscsiscan', ['portal'], { node => $nodename }, sub {
734 my $res = shift;
735
736 my $maxlen = 0;
737 foreach my $rec (@$res) {
738 my $len = length ($rec->{target});
739 $maxlen = $len if $len > $maxlen;
740 }
741 foreach my $rec (@$res) {
742 printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
743 }
744 }],
745 lvm => [ __PACKAGE__, 'lvmscan', [], { node => $nodename }, sub {
746 my $res = shift;
747 foreach my $rec (@$res) {
748 printf "$rec->{vg}\n";
749 }
750 }],
751 lvmthin => [ __PACKAGE__, 'lvmthinscan', ['vg'], { node => $nodename }, sub {
752 my $res = shift;
753 foreach my $rec (@$res) {
754 printf "$rec->{lv}\n";
755 }
756 }],
757 zfs => [ __PACKAGE__, 'zfsscan', [], { node => $nodename }, sub {
758 my $res = shift;
759
760 foreach my $rec (@$res) {
761 printf "$rec->{pool}\n";
762 }
763 }],
764 },
765 nfsscan => { alias => 'scan nfs' },
766 cifsscan => { alias => 'scan cifs' },
767 glusterfsscan => { alias => 'scan glusterfs' },
768 iscsiscan => { alias => 'scan iscsi' },
769 lvmscan => { alias => 'scan lvm' },
770 lvmthinscan => { alias => 'scan lvmthin' },
771 zfsscan => { alias => 'scan zfs' },
772 path => [ __PACKAGE__, 'path', ['volume']],
773 extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
774 export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
775 import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename']],
776 };
777
778 1;