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