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