]> git.proxmox.com Git - pve-storage.git/blame - PVE/CLI/pvesm.pm
volume export/import: allow uppercase letters
[pve-storage.git] / PVE / CLI / pvesm.pm
CommitLineData
c669f42d
DM
1package PVE::CLI::pvesm;
2
3use strict;
4use warnings;
5
9559a62a 6use POSIX qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC);
c669f42d
DM
7use Fcntl ':flock';
8use File::Path;
9
10use PVE::SafeSyslog;
11use PVE::Cluster;
12use PVE::INotify;
13use PVE::RPCEnvironment;
14use PVE::Storage;
25a95836 15use PVE::Tools qw(extract_param);
c669f42d
DM
16use PVE::API2::Storage::Config;
17use PVE::API2::Storage::Content;
25a95836 18use PVE::API2::Storage::PruneBackups;
2892b673 19use PVE::API2::Storage::Scan;
c669f42d 20use PVE::API2::Storage::Status;
c669f42d 21use PVE::JSONSchema qw(get_standard_option);
c26f3a71 22use PVE::PTY;
c669f42d
DM
23
24use PVE::CLIHandler;
25
26use base qw(PVE::CLIHandler);
27
9559a62a 28my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs'];
47f37b53 29
c669f42d
DM
30my $nodename = PVE::INotify::nodename();
31
42f2c57d
DC
32sub param_mapping {
33 my ($name) = @_;
34
35 my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', {
36 func => sub {
37 my ($value) = @_;
38 return $value if $value;
39 return PVE::PTY::read_password("Enter Password: ");
40 },
41 });
baf77120 42
0ca8eb4f
WB
43 my $enc_key_map = {
44 name => 'encryption-key',
45 desc => 'a file containing an encryption key, or the special value "autogen"',
46 func => sub {
47 my ($value) = @_;
48 return $value if $value eq 'autogen';
49 return PVE::Tools::file_get_contents($value);
50 }
51 };
52
53
42f2c57d
DC
54 my $mapping = {
55 'cifsscan' => [ $password_map ],
11a942a1 56 'cifs' => [ $password_map ],
39fc0c77 57 'pbs' => [ $password_map ],
0ca8eb4f
WB
58 'create' => [ $password_map, $enc_key_map ],
59 'update' => [ $password_map, $enc_key_map ],
42f2c57d
DC
60 };
61 return $mapping->{$name};
c26f3a71
WL
62}
63
f984732e
DM
64sub setup_environment {
65 PVE::RPCEnvironment->setup_default_cli_env();
66}
67
5f184292
FE
68__PACKAGE__->register_method ({
69 name => 'apiinfo',
70 path => 'apiinfo',
71 method => 'GET',
72 description => "Returns APIVER and APIAGE.",
73 parameters => {
74 additionalProperties => 0,
75 properties => {},
76 },
77 returns => {
78 type => 'object',
79 properties => {
80 apiver => { type => 'integer' },
81 apiage => { type => 'integer' },
82 },
83 },
84 code => sub {
85 return {
86 apiver => PVE::Storage::APIVER,
87 apiage => PVE::Storage::APIAGE,
88 };
89 }
90});
91
c669f42d
DM
92__PACKAGE__->register_method ({
93 name => 'path',
94 path => 'path',
95 method => 'GET',
96 description => "Get filesystem path for specified volume",
97 parameters => {
98 additionalProperties => 0,
99 properties => {
100 volume => {
101 description => "Volume identifier",
102 type => 'string', format => 'pve-volume-id',
f3bd890d 103 completion => \&PVE::Storage::complete_volume,
c669f42d
DM
104 },
105 },
106 },
107 returns => { type => 'null' },
108
109 code => sub {
110 my ($param) = @_;
111
112 my $cfg = PVE::Storage::config();
113
114 my $path = PVE::Storage::path ($cfg, $param->{volume});
115
116 print "$path\n";
117
118 return undef;
119
120 }});
121
fa017b96
FG
122__PACKAGE__->register_method ({
123 name => 'extractconfig',
124 path => 'extractconfig',
125 method => 'GET',
126 description => "Extract configuration from vzdump backup archive.",
127 permissions => {
128 description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
129 user => 'all',
130 },
131 protected => 1,
132 parameters => {
133 additionalProperties => 0,
134 properties => {
135 volume => {
136 description => "Volume identifier",
137 type => 'string',
138 completion => \&PVE::Storage::complete_volume,
139 },
140 },
141 },
142 returns => { type => 'null' },
143 code => sub {
144 my ($param) = @_;
145 my $volume = $param->{volume};
146
147 my $rpcenv = PVE::RPCEnvironment::get();
148 my $authuser = $rpcenv->get_user();
149
150 my $storage_cfg = PVE::Storage::config();
04a13668 151 PVE::Storage::check_volume_access($rpcenv, $authuser, $storage_cfg, undef, $volume);
fa017b96
FG
152
153 my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
154
155 print "$config_raw\n";
156 return;
157 }});
158
c669f42d
DM
159my $print_content = sub {
160 my ($list) = @_;
161
61c261e7 162 my ($maxlenname, $maxsize) = (0, 0);
c669f42d 163 foreach my $info (@$list) {
c669f42d
DM
164 my $volid = $info->{volid};
165 my $sidlen = length ($volid);
166 $maxlenname = $sidlen if $sidlen > $maxlenname;
61c261e7 167 $maxsize = $info->{size} if ($info->{size} // 0) > $maxsize;
c669f42d 168 }
61c261e7
TL
169 my $sizemaxdigits = length($maxsize);
170
171 my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s";
172 printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID";
c669f42d
DM
173
174 foreach my $info (@$list) {
175 next if !$info->{vmid};
176 my $volid = $info->{volid};
177
61c261e7 178 printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid};
c669f42d
DM
179 }
180
181 foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
182 next if $info->{vmid};
183 my $volid = $info->{volid};
184
61c261e7 185 printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $info->{size};
c669f42d
DM
186 }
187};
188
189my $print_status = sub {
190 my $res = shift;
191
192 my $maxlen = 0;
193 foreach my $res (@$res) {
194 my $storeid = $res->{storage};
195 $maxlen = length ($storeid) if length ($storeid) > $maxlen;
196 }
197 $maxlen+=1;
198
d40e27de
TL
199 printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type',
200 'Status', 'Total', 'Used', 'Available', '%';
201
c669f42d
DM
202 foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
203 my $storeid = $res->{storage};
204
d40e27de
TL
205 my $active = $res->{active} ? 'active' : 'inactive';
206 my ($per, $per_fmt) = (0, '% 7.2f%%');
207 $per = ($res->{used}*100)/$res->{total} if $res->{total} > 0;
208
209 if (!$res->{enabled}) {
04301013 210 $per = 'N/A';
d40e27de
TL
211 $per_fmt = '% 8s';
212 $active = 'disabled';
213 }
c669f42d 214
d40e27de
TL
215 printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid,
216 $res->{type}, $active, $res->{total}/1024, $res->{used}/1024,
217 $res->{avail}/1024, $per;
c669f42d
DM
218 }
219};
220
47f37b53
WB
221__PACKAGE__->register_method ({
222 name => 'export',
223 path => 'export',
224 method => 'GET',
a43a796c 225 description => "Used internally to export a volume.",
47f37b53
WB
226 protected => 1,
227 parameters => {
228 additionalProperties => 0,
229 properties => {
230 volume => {
231 description => "Volume identifier",
232 type => 'string',
233 completion => \&PVE::Storage::complete_volume,
234 },
235 format => {
236 description => "Export stream format",
237 type => 'string',
238 enum => $KNOWN_EXPORT_FORMATS,
239 },
240 filename => {
241 description => "Destination file name",
242 type => 'string',
243 },
244 base => {
245 description => "Snapshot to start an incremental stream from",
246 type => 'string',
bef7920d 247 pattern => qr/[a-z0-9_\-]{1,40}/i,
47f37b53
WB
248 maxLength => 40,
249 optional => 1,
250 },
251 snapshot => {
252 description => "Snapshot to export",
253 type => 'string',
bef7920d 254 pattern => qr/[a-z0-9_\-]{1,40}/i,
47f37b53
WB
255 maxLength => 40,
256 optional => 1,
257 },
258 'with-snapshots' => {
259 description =>
260 "Whether to include intermediate snapshots in the stream",
261 type => 'boolean',
262 optional => 1,
263 default => 0,
264 },
265 },
266 },
267 returns => { type => 'null' },
268 code => sub {
269 my ($param) = @_;
270
271 my $filename = $param->{filename};
272
273 my $outfh;
274 if ($filename eq '-') {
275 $outfh = \*STDOUT;
276 } else {
9559a62a 277 sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC)
47f37b53
WB
278 or die "open($filename): $!\n";
279 }
280
281 eval {
282 my $cfg = PVE::Storage::config();
283 PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format},
284 $param->{snapshot}, $param->{base}, $param->{'with-snapshots'});
285 };
286 my $err = $@;
287 if ($filename ne '-') {
288 close($outfh);
289 unlink($filename) if $err;
290 }
291 die $err if $err;
292 return;
293 }
294});
295
296__PACKAGE__->register_method ({
297 name => 'import',
298 path => 'import',
299 method => 'PUT',
a43a796c 300 description => "Used internally to import a volume.",
47f37b53
WB
301 protected => 1,
302 parameters => {
303 additionalProperties => 0,
304 properties => {
305 volume => {
306 description => "Volume identifier",
307 type => 'string',
308 completion => \&PVE::Storage::complete_volume,
309 },
310 format => {
311 description => "Import stream format",
312 type => 'string',
313 enum => $KNOWN_EXPORT_FORMATS,
314 },
315 filename => {
228e5be9
TL
316 description => "Source file name. For '-' stdin is used, the " .
317 "tcp://<IP-or-CIDR> format allows to use a TCP connection as input. " .
318 "Else, the file is treated as common file.",
47f37b53
WB
319 type => 'string',
320 },
321 base => {
322 description => "Base snapshot of an incremental stream",
323 type => 'string',
bef7920d 324 pattern => qr/[a-z0-9_\-]{1,40}/i,
47f37b53
WB
325 maxLength => 40,
326 optional => 1,
327 },
328 'with-snapshots' => {
329 description =>
330 "Whether the stream includes intermediate snapshots",
331 type => 'boolean',
332 optional => 1,
333 default => 0,
334 },
52595938
WB
335 'delete-snapshot' => {
336 description => "A snapshot to delete on success",
337 type => 'string',
bef7920d 338 pattern => qr/[a-z0-9_\-]{1,80}/i,
52595938
WB
339 maxLength => 80,
340 optional => 1,
341 },
a97d3ee4
FE
342 'allow-rename' => {
343 description => "Choose a new volume ID if the requested " .
344 "volume ID already exists, instead of throwing an error.",
345 type => 'boolean',
346 optional => 1,
347 default => 0,
348 },
47f37b53
WB
349 },
350 },
a97d3ee4 351 returns => { type => 'string' },
47f37b53
WB
352 code => sub {
353 my ($param) = @_;
354
355 my $filename = $param->{filename};
356
357 my $infh;
358 if ($filename eq '-') {
359 $infh = \*STDIN;
228e5be9
TL
360 } elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) {
361 my ($cidr, $ip, $subnet) = ($1, $2, $3);
362 if ($subnet) { # got real CIDR notation, not just IP
a2aae38c 363 my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
ed2df8e3
TL
364 die "Unable to get any local IP address in network '$cidr'\n"
365 if scalar(@$ips) < 1;
366 die "Got multiple local IP address in network '$cidr'\n"
367 if scalar(@$ips) > 1;
368
369 $ip = $ips->[0];
228e5be9
TL
370 }
371 my $family = PVE::Tools::get_host_address_family($ip);
372 my $port = PVE::Tools::next_migrate_port($family, $ip);
373
374 my $sock_params = {
375 Listen => 1,
376 ReuseAddr => 1,
377 Proto => &Socket::IPPROTO_TCP,
378 GetAddrInfoFlags => 0,
379 LocalAddr => $ip,
380 LocalPort => $port,
381 };
382 my $socket = IO::Socket::IP->new(%$sock_params)
383 or die "failed to open socket: $!\n";
384
385 print "$ip\n$port\n"; # tell remote where to connect
386 *STDOUT->flush();
387
388 my $prev_alarm = alarm 0;
389 local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
390 alarm 30;
391 my $client = $socket->accept; # Wait for a client
392 alarm $prev_alarm;
393 close($socket);
394
395 $infh = \*$client;
47f37b53 396 } else {
9559a62a 397 sysopen($infh, $filename, O_RDONLY)
47f37b53
WB
398 or die "open($filename): $!\n";
399 }
400
401 my $cfg = PVE::Storage::config();
52595938
WB
402 my $volume = $param->{volume};
403 my $delete = $param->{'delete-snapshot'};
a97d3ee4
FE
404 my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
405 $param->{base}, $param->{'with-snapshots'}, $param->{'allow-rename'});
406 PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete)
52595938 407 if defined($delete);
a97d3ee4 408 return $imported_volid;
47f37b53
WB
409 }
410});
411
25a95836
FE
412__PACKAGE__->register_method ({
413 name => 'prunebackups',
414 path => 'prunebackups',
415 method => 'GET',
8ca00a63
FE
416 description => "Prune backups. Only those using the standard naming scheme are considered. " .
417 "If no keep options are specified, those from the storage configuration are used.",
25a95836
FE
418 protected => 1,
419 proxyto => 'node',
420 parameters => {
421 additionalProperties => 0,
422 properties => {
423 'dry-run' => {
424 description => "Only show what would be pruned, don't delete anything.",
425 type => 'boolean',
426 optional => 1,
427 },
428 node => get_standard_option('pve-node'),
429 storage => get_standard_option('pve-storage-id', {
430 completion => \&PVE::Storage::complete_storage_enabled,
431 }),
a0933d7e 432 %{$PVE::Storage::Plugin::prune_backups_format},
25a95836
FE
433 type => {
434 description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
435 type => 'string',
436 optional => 1,
437 enum => ['qemu', 'lxc'],
438 },
439 vmid => get_standard_option('pve-vmid', {
440 description => "Only consider backups for this guest.",
441 optional => 1,
442 completion => \&PVE::Cluster::complete_vmid,
443 }),
444 },
445 },
446 returns => {
447 type => 'object',
448 properties => {
449 dryrun => {
450 description => 'If it was a dry run or not. The list will only be defined in that case.',
451 type => 'boolean',
452 },
453 list => {
454 type => 'array',
455 items => {
456 type => 'object',
457 properties => {
458 volid => {
459 description => "Backup volume ID.",
460 type => 'string',
461 },
462 'ctime' => {
463 description => "Creation time of the backup (seconds since the UNIX epoch).",
464 type => 'integer',
465 },
466 'mark' => {
467 description => "Whether the backup would be kept or removed. For backups that don't " .
468 "use the standard naming scheme, it's 'protected'.",
469 type => 'string',
470 },
471 type => {
472 description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
473 type => 'string',
474 },
475 'vmid' => {
476 description => "The VM the backup belongs to.",
477 type => 'integer',
478 optional => 1,
479 },
480 },
481 },
482 },
483 },
484 },
485 code => sub {
486 my ($param) = @_;
487
488 my $dryrun = extract_param($param, 'dry-run') ? 1 : 0;
489
a0933d7e
FE
490 my $keep_opts;
491 foreach my $keep (keys %{$PVE::Storage::Plugin::prune_backups_format}) {
492 $keep_opts->{$keep} = extract_param($param, $keep) if defined($param->{$keep});
493 }
494 $param->{'prune-backups'} = PVE::JSONSchema::print_property_string(
495 $keep_opts, $PVE::Storage::Plugin::prune_backups_format) if $keep_opts;
496
25a95836
FE
497 my $list = [];
498 if ($dryrun) {
499 $list = PVE::API2::Storage::PruneBackups->dryrun($param);
500 } else {
501 PVE::API2::Storage::PruneBackups->delete($param);
502 }
503
504 return {
505 dryrun => $dryrun,
506 list => $list,
507 };
508 }});
509
39fc0c77
TL
510my $print_api_result = sub {
511 my ($data, $schema, $options) = @_;
512 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
513};
514
c669f42d
DM
515our $cmddef = {
516 add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
517 set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
518 remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
519 status => [ "PVE::API2::Storage::Status", 'index', [],
520 { node => $nodename }, $print_status ],
521 list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
522 { node => $nodename }, $print_content ],
523 alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
524 { node => $nodename }, sub {
525 my $volid = shift;
e967e0ef 526 print "successfully created '$volid'\n";
c669f42d
DM
527 }],
528 free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
529 { node => $nodename } ],
957321a8 530 scan => {
2892b673 531 nfs => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'], { node => $nodename }, sub {
957321a8
TL
532 my $res = shift;
533
534 my $maxlen = 0;
535 foreach my $rec (@$res) {
536 my $len = length ($rec->{path});
537 $maxlen = $len if $len > $maxlen;
538 }
539 foreach my $rec (@$res) {
540 printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
541 }
542 }],
2892b673 543 cifs => [ "PVE::API2::Storage::Scan", 'cifsscan', ['server'], { node => $nodename }, sub {
957321a8
TL
544 my $res = shift;
545
546 my $maxlen = 0;
547 foreach my $rec (@$res) {
548 my $len = length ($rec->{share});
549 $maxlen = $len if $len > $maxlen;
550 }
551 foreach my $rec (@$res) {
552 printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
553 }
554 }],
2892b673 555 glusterfs => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], { node => $nodename }, sub {
957321a8
TL
556 my $res = shift;
557
558 foreach my $rec (@$res) {
559 printf "%s\n", $rec->{volname};
560 }
561 }],
2892b673 562 iscsi => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['portal'], { node => $nodename }, sub {
957321a8
TL
563 my $res = shift;
564
565 my $maxlen = 0;
566 foreach my $rec (@$res) {
567 my $len = length ($rec->{target});
568 $maxlen = $len if $len > $maxlen;
569 }
570 foreach my $rec (@$res) {
571 printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
572 }
573 }],
2892b673 574 lvm => [ "PVE::API2::Storage::Scan", 'lvmscan', [], { node => $nodename }, sub {
957321a8
TL
575 my $res = shift;
576 foreach my $rec (@$res) {
577 printf "$rec->{vg}\n";
578 }
579 }],
2892b673 580 lvmthin => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], { node => $nodename }, sub {
957321a8
TL
581 my $res = shift;
582 foreach my $rec (@$res) {
583 printf "$rec->{lv}\n";
584 }
585 }],
39fc0c77
TL
586 pbs => [
587 "PVE::API2::Storage::Scan",
588 'pbsscan',
589 ['server', 'username'],
590 { node => $nodename },
591 $print_api_result,
592 $PVE::RESTHandler::standard_output_options,
593 ],
2892b673 594 zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', [], { node => $nodename }, sub {
957321a8
TL
595 my $res = shift;
596
597 foreach my $rec (@$res) {
598 printf "$rec->{pool}\n";
599 }
600 }],
601 },
602 nfsscan => { alias => 'scan nfs' },
603 cifsscan => { alias => 'scan cifs' },
604 glusterfsscan => { alias => 'scan glusterfs' },
605 iscsiscan => { alias => 'scan iscsi' },
606 lvmscan => { alias => 'scan lvm' },
607 lvmthinscan => { alias => 'scan lvmthin' },
608 zfsscan => { alias => 'scan zfs' },
c669f42d 609 path => [ __PACKAGE__, 'path', ['volume']],
fa017b96 610 extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
47f37b53 611 export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
a97d3ee4
FE
612 import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub {
613 my $volid = shift;
614 print PVE::Storage::volume_imported_message($volid);
615 }],
5f184292
FE
616 apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub {
617 my $res = shift;
618
619 print "APIVER $res->{apiver}\n";
620 print "APIAGE $res->{apiage}\n";
621 }],
25a95836
FE
622 'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => $nodename }, sub {
623 my $res = shift;
624
625 my ($dryrun, $list) = ($res->{dryrun}, $res->{list});
626
627 return if !$dryrun;
628
c3e87d0f
FE
629 if (!scalar(@{$list})) {
630 print "No backups found\n";
631 return;
632 }
633
7b73d327
FE
634 print "NOTE: this is only a preview and might not be what a subsequent\n" .
635 "prune call does if backups are removed/added in the meantime.\n\n";
25a95836
FE
636
637 my @sorted = sort {
638 my $vmcmp = PVE::Tools::safe_compare($a->{vmid}, $b->{vmid}, sub { $_[0] <=> $_[1] });
639 return $vmcmp if $vmcmp ne 0;
640 return $a->{ctime} <=> $b->{ctime};
641 } @{$list};
642
643 my $maxlen = 0;
644 foreach my $backup (@sorted) {
645 my $volid = $backup->{volid};
646 $maxlen = length($volid) if length($volid) > $maxlen;
647 }
648 $maxlen+=1;
649
650 printf("%-${maxlen}s %15s %10s\n", 'Backup', 'Backup-ID', 'Prune-Mark');
651 foreach my $backup (@sorted) {
652 my $type = $backup->{type};
653 my $vmid = $backup->{vmid};
654 my $backup_id = defined($vmid) ? "$type/$vmid" : "$type";
655 printf("%-${maxlen}s %15s %10s\n", $backup->{volid}, $backup_id, $backup->{mark});
656 }
657 }],
c669f42d
DM
658};
659
6601;