]> git.proxmox.com Git - pve-manager.git/blame - PVE/API2/Backup.pm
ui: render storage type: improve fallback
[pve-manager.git] / PVE / API2 / Backup.pm
CommitLineData
ac27b58d
DM
1package PVE::API2::Backup;
2
3use strict;
4use warnings;
52878b0a 5use Digest::SHA;
1c87d344 6use UUID qw(uuid);
ac27b58d
DM
7
8use PVE::SafeSyslog;
9use PVE::Tools qw(extract_param);
2424074e 10use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
ac27b58d
DM
11use PVE::RESTHandler;
12use PVE::RPCEnvironment;
13use PVE::JSONSchema;
14use PVE::Storage;
15use PVE::Exception qw(raise_param_exc);
16use PVE::VZDump;
2424074e 17use PVE::VZDump::Common;
305921b1 18use PVE::Jobs; # for VZDump Jobs
9ee99910 19use Proxmox::RS::CalendarEvent;
ac27b58d
DM
20
21use base qw(PVE::RESTHandler);
22
85b9ba88
DC
23use constant ALL_DAYS => 'mon,tue,wed,thu,fri,sat,sun';
24
ac27b58d
DM
25PVE::JSONSchema::register_format('pve-day-of-week', \&verify_day_of_week);
26sub verify_day_of_week {
27 my ($value, $noerr) = @_;
28
29 return $value if $value =~ m/^(mon|tue|wed|thu|fri|sat|sun)$/;
30
31 return undef if $noerr;
32
33 die "invalid day '$value'\n";
34}
35
43b2494b
SR
36my $vzdump_job_id_prop = {
37 type => 'string',
38 description => "The job ID.",
39 maxLength => 50
40};
ac27b58d 41
2617768f
TL
42my $assert_param_permission = sub {
43 my ($param, $user) = @_;
44 return if $user eq 'root@pam'; # always OK
45
46 for my $key (qw(tmpdir dumpdir script)) {
47 raise_param_exc({ $key => "Only root may set this option."}) if exists $param->{$key};
48 }
49};
50
305921b1
DC
51my $convert_to_schedule = sub {
52 my ($job) = @_;
53
54 my $starttime = $job->{starttime};
de92b4d7 55
5cc0c3a0 56 return "$starttime" if !$job->{dow}; # dow is restrictive, so none means all days
305921b1 57
5cc0c3a0
TL
58 # normalize as it could be a null-separated list previously
59 my $dow = join(',', PVE::Tools::split_list($job->{dow}));
305921b1 60
5cc0c3a0 61 return $dow eq ALL_DAYS ? "$starttime" : "$dow $starttime";
305921b1
DC
62};
63
64my $schedule_param_check = sub {
65 my ($param) = @_;
66 if (defined($param->{schedule})) {
67 if (defined($param->{starttime})) {
68 raise_param_exc({ starttime => "'starttime' and 'schedule' cannot both be set" });
69 }
70 } elsif (!defined($param->{starttime})) {
71 raise_param_exc({ schedule => "neither 'starttime' nor 'schedule' were set" });
72 } else {
73 $param->{schedule} = $convert_to_schedule->($param);
74 }
75
76 delete $param->{starttime};
77 delete $param->{dow};
78};
79
ac27b58d 80__PACKAGE__->register_method({
60e049c2
TM
81 name => 'index',
82 path => '',
ac27b58d
DM
83 method => 'GET',
84 description => "List vzdump backup schedule.",
937515d6
DM
85 permissions => {
86 check => ['perm', '/', ['Sys.Audit']],
87 },
ac27b58d
DM
88 parameters => {
89 additionalProperties => 0,
90 properties => {},
91 },
92 returns => {
93 type => 'array',
94 items => {
95 type => "object",
96 properties => {
43b2494b 97 id => $vzdump_job_id_prop
ac27b58d
DM
98 },
99 },
100 links => [ { rel => 'child', href => "{id}" } ],
101 },
102 code => sub {
103 my ($param) = @_;
104
105 my $rpcenv = PVE::RPCEnvironment::get();
106 my $user = $rpcenv->get_user();
107
b0905e3a 108 my $data = cfs_read_file('vzdump.cron');
305921b1
DC
109 my $jobs_data = cfs_read_file('jobs.cfg');
110 my $order = $jobs_data->{order};
111 my $jobs = $jobs_data->{ids};
ac27b58d
DM
112
113 my $res = $data->{jobs} || [];
305921b1
DC
114 foreach my $job (@$res) {
115 $job->{schedule} = $convert_to_schedule->($job);
116 }
117
118 foreach my $jobid (sort { $order->{$a} <=> $order->{$b} } keys %$jobs) {
119 my $job = $jobs->{$jobid};
120 next if $job->{type} ne 'vzdump';
c048ad30
DC
121
122 if (my $schedule = $job->{schedule}) {
123 # vzdump jobs are cluster wide, there maybe was no local run
124 # so simply calculate from now
125 my $last_run = time();
9ee99910 126 my $calspec = Proxmox::RS::CalendarEvent->new($schedule);
c048ad30
DC
127 my $next_run = $calspec->compute_next_event($last_run);
128 $job->{'next-run'} = $next_run if defined($next_run);
129 }
130
305921b1
DC
131 push @$res, $job;
132 }
ac27b58d
DM
133
134 return $res;
135 }});
136
137__PACKAGE__->register_method({
60e049c2
TM
138 name => 'create_job',
139 path => '',
ac27b58d
DM
140 method => 'POST',
141 protected => 1,
142 description => "Create new vzdump backup job.",
937515d6
DM
143 permissions => {
144 check => ['perm', '/', ['Sys.Modify']],
f0bbc084 145 description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
937515d6 146 },
ac27b58d
DM
147 parameters => {
148 additionalProperties => 0,
2424074e 149 properties => PVE::VZDump::Common::json_config_properties({
305921b1
DC
150 id => {
151 type => 'string',
152 description => "Job ID (will be autogenerated).",
153 format => 'pve-configid',
154 optional => 1, # FIXME: make required on 8.0
155 },
156 schedule => {
157 description => "Backup schedule. The format is a subset of `systemd` calendar events.",
158 type => 'string', format => 'pve-calendar-event',
159 maxLength => 128,
160 optional => 1,
161 },
7625ea19
DM
162 starttime => {
163 type => 'string',
164 description => "Job Start time.",
165 pattern => '\d{1,2}:\d{1,2}',
166 typetext => 'HH:MM',
305921b1 167 optional => 1,
ac27b58d
DM
168 },
169 dow => {
170 type => 'string', format => 'pve-day-of-week-list',
171 optional => 1,
172 description => "Day of week selection.",
305921b1 173 requires => 'starttime',
85b9ba88 174 default => ALL_DAYS,
ac27b58d 175 },
4341db1d
TL
176 enabled => {
177 type => 'boolean',
178 optional => 1,
179 description => "Enable or disable the job.",
180 default => '1',
181 },
998b61fb
DC
182 comment => {
183 optional => 1,
184 type => 'string',
185 description => "Description for the Job.",
186 maxLength => 512,
187 },
ac27b58d
DM
188 }),
189 },
190 returns => { type => 'null' },
191 code => sub {
192 my ($param) = @_;
193
194 my $rpcenv = PVE::RPCEnvironment::get();
195 my $user = $rpcenv->get_user();
196
2617768f 197 $assert_param_permission->($param, $user);
f0bbc084 198
c92c54d5
TL
199 if (my $pool = $param->{pool}) {
200 $rpcenv->check_pool_exist($pool);
201 $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
202 }
203
305921b1 204 $schedule_param_check->($param);
c92c54d5 205
305921b1
DC
206 $param->{enabled} = 1 if !defined($param->{enabled});
207
208 # autogenerate id for api compatibility FIXME remove with 8.0
ca62f164 209 my $id = extract_param($param, 'id') // UUID::uuid();
305921b1
DC
210
211 cfs_lock_file('jobs.cfg', undef, sub {
212 my $data = cfs_read_file('jobs.cfg');
213
214 die "Job '$id' already exists\n"
215 if $data->{ids}->{$id};
ac27b58d 216
200cef80 217 PVE::VZDump::verify_vzdump_parameters($param, 1);
305921b1
DC
218 my $plugin = PVE::Jobs::Plugin->lookup('vzdump');
219 my $opts = $plugin->check_config($id, $param, 1, 1);
ac27b58d 220
305921b1 221 $data->{ids}->{$id} = $opts;
ac27b58d 222
305921b1
DC
223 PVE::Jobs::create_job($id, 'vzdump');
224
225 cfs_write_file('jobs.cfg', $data);
226 });
200cef80 227 die "$@" if ($@);
ac27b58d
DM
228
229 return undef;
230 }});
231
232__PACKAGE__->register_method({
60e049c2
TM
233 name => 'read_job',
234 path => '{id}',
ac27b58d
DM
235 method => 'GET',
236 description => "Read vzdump backup job definition.",
937515d6
DM
237 permissions => {
238 check => ['perm', '/', ['Sys.Audit']],
239 },
ac27b58d
DM
240 parameters => {
241 additionalProperties => 0,
242 properties => {
43b2494b 243 id => $vzdump_job_id_prop
ac27b58d
DM
244 },
245 },
246 returns => {
247 type => 'object',
248 },
249 code => sub {
250 my ($param) = @_;
251
252 my $rpcenv = PVE::RPCEnvironment::get();
253 my $user = $rpcenv->get_user();
254
b0905e3a 255 my $data = cfs_read_file('vzdump.cron');
ac27b58d
DM
256
257 my $jobs = $data->{jobs} || [];
258
259 foreach my $job (@$jobs) {
305921b1
DC
260 if ($job->{id} eq $param->{id}) {
261 $job->{schedule} = $convert_to_schedule->($job);
262 return $job;
263 }
ac27b58d
DM
264 }
265
305921b1
DC
266 my $jobs_data = cfs_read_file('jobs.cfg');
267 my $job = $jobs_data->{ids}->{$param->{id}};
268 return $job if $job && $job->{type} eq 'vzdump';
269
ac27b58d
DM
270 raise_param_exc({ id => "No such job '$param->{id}'" });
271
272 }});
273
274__PACKAGE__->register_method({
60e049c2
TM
275 name => 'delete_job',
276 path => '{id}',
ac27b58d
DM
277 method => 'DELETE',
278 description => "Delete vzdump backup job definition.",
937515d6
DM
279 permissions => {
280 check => ['perm', '/', ['Sys.Modify']],
281 },
ac27b58d
DM
282 protected => 1,
283 parameters => {
284 additionalProperties => 0,
285 properties => {
43b2494b 286 id => $vzdump_job_id_prop
ac27b58d
DM
287 },
288 },
289 returns => { type => 'null' },
290 code => sub {
291 my ($param) = @_;
292
293 my $rpcenv = PVE::RPCEnvironment::get();
294 my $user = $rpcenv->get_user();
295
305921b1
DC
296 my $id = $param->{id};
297
200cef80
CE
298 my $delete_job = sub {
299 my $data = cfs_read_file('vzdump.cron');
ac27b58d 300
200cef80
CE
301 my $jobs = $data->{jobs} || [];
302 my $newjobs = [];
ac27b58d 303
200cef80
CE
304 my $found;
305 foreach my $job (@$jobs) {
305921b1 306 if ($job->{id} eq $id) {
200cef80
CE
307 $found = 1;
308 } else {
309 push @$newjobs, $job;
310 }
ac27b58d 311 }
ac27b58d 312
305921b1
DC
313 if (!$found) {
314 cfs_lock_file('jobs.cfg', undef, sub {
315 my $jobs_data = cfs_read_file('jobs.cfg');
316
317 if (!defined($jobs_data->{ids}->{$id})) {
318 raise_param_exc({ id => "No such job '$id'" });
319 }
320 delete $jobs_data->{ids}->{$id};
321
322 PVE::Jobs::remove_job($id, 'vzdump');
ac27b58d 323
305921b1
DC
324 cfs_write_file('jobs.cfg', $jobs_data);
325 });
326 die "$@" if $@;
327 } else {
328 $data->{jobs} = $newjobs;
ac27b58d 329
305921b1
DC
330 cfs_write_file('vzdump.cron', $data);
331 }
200cef80
CE
332 };
333 cfs_lock_file('vzdump.cron', undef, $delete_job);
334 die "$@" if ($@);
ac27b58d
DM
335
336 return undef;
337 }});
338
339__PACKAGE__->register_method({
60e049c2
TM
340 name => 'update_job',
341 path => '{id}',
ac27b58d
DM
342 method => 'PUT',
343 protected => 1,
344 description => "Update vzdump backup job definition.",
937515d6
DM
345 permissions => {
346 check => ['perm', '/', ['Sys.Modify']],
e6d963ca 347 description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
937515d6 348 },
ac27b58d
DM
349 parameters => {
350 additionalProperties => 0,
2424074e 351 properties => PVE::VZDump::Common::json_config_properties({
43b2494b 352 id => $vzdump_job_id_prop,
305921b1
DC
353 schedule => {
354 description => "Backup schedule. The format is a subset of `systemd` calendar events.",
355 type => 'string', format => 'pve-calendar-event',
356 maxLength => 128,
357 optional => 1,
358 },
7625ea19
DM
359 starttime => {
360 type => 'string',
361 description => "Job Start time.",
362 pattern => '\d{1,2}:\d{1,2}',
363 typetext => 'HH:MM',
305921b1 364 optional => 1,
ac27b58d
DM
365 },
366 dow => {
367 type => 'string', format => 'pve-day-of-week-list',
368 optional => 1,
305921b1 369 requires => 'starttime',
ac27b58d
DM
370 description => "Day of week selection.",
371 },
53c6bb6c
DM
372 delete => {
373 type => 'string', format => 'pve-configid-list',
374 description => "A list of settings you want to delete.",
375 optional => 1,
376 },
4341db1d
TL
377 enabled => {
378 type => 'boolean',
379 optional => 1,
380 description => "Enable or disable the job.",
381 default => '1',
382 },
998b61fb
DC
383 comment => {
384 optional => 1,
385 type => 'string',
386 description => "Description for the Job.",
387 maxLength => 512,
388 },
ac27b58d
DM
389 }),
390 },
391 returns => { type => 'null' },
392 code => sub {
393 my ($param) = @_;
394
395 my $rpcenv = PVE::RPCEnvironment::get();
396 my $user = $rpcenv->get_user();
397
2617768f 398 $assert_param_permission->($param, $user);
d5b9f2e1 399
16f5b283
TL
400 if (my $pool = $param->{pool}) {
401 $rpcenv->check_pool_exist($pool);
402 $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
403 }
404
305921b1
DC
405 $schedule_param_check->($param);
406
407 my $id = extract_param($param, 'id');
408 my $delete = extract_param($param, 'delete');
409 if ($delete) {
410 $delete = [PVE::Tools::split_list($delete)];
411 }
412
200cef80
CE
413 my $update_job = sub {
414 my $data = cfs_read_file('vzdump.cron');
305921b1 415 my $jobs_data = cfs_read_file('jobs.cfg');
ac27b58d 416
200cef80 417 my $jobs = $data->{jobs} || [];
ac27b58d 418
200cef80 419 die "no options specified\n" if !scalar(keys %$param);
53c6bb6c 420
200cef80 421 PVE::VZDump::verify_vzdump_parameters($param);
305921b1
DC
422 my $plugin = PVE::Jobs::Plugin->lookup('vzdump');
423 my $opts = $plugin->check_config($id, $param, 0, 1);
424
425 # try to find it in old vzdump.cron and convert it to a job
426 my ($idx) = grep { $jobs->[$_]->{id} eq $id } (0 .. scalar(@$jobs) - 1);
427
428 my $job;
429 if (defined($idx)) {
430 $job = splice @$jobs, $idx, 1;
431 $job->{schedule} = $convert_to_schedule->($job);
432 delete $job->{starttime};
433 delete $job->{dow};
434 delete $job->{id};
435 $job->{type} = 'vzdump';
436 $jobs_data->{ids}->{$id} = $job;
437 } else {
438 $job = $jobs_data->{ids}->{$id};
439 die "no such vzdump job\n" if !$job || $job->{type} ne 'vzdump';
440 }
ac27b58d 441
305921b1 442 foreach my $k (@$delete) {
998b61fb 443 if (!PVE::VZDump::option_exists($k) && $k ne 'comment') {
305921b1
DC
444 raise_param_exc({ delete => "unknown option '$k'" });
445 }
ac27b58d 446
305921b1
DC
447 delete $job->{$k};
448 }
53c6bb6c 449
305921b1
DC
450 my $schedule_updated = 0;
451 if ($param->{schedule} ne $job->{schedule}) {
452 $schedule_updated = 1;
453 }
53c6bb6c 454
305921b1
DC
455 foreach my $k (keys %$param) {
456 $job->{$k} = $param->{$k};
457 }
ac27b58d 458
305921b1
DC
459 $job->{all} = 1 if (defined($job->{exclude}) && !defined($job->{pool}));
460
461 if (defined($param->{vmid})) {
462 delete $job->{all};
463 delete $job->{exclude};
464 delete $job->{pool};
465 } elsif ($param->{all}) {
466 delete $job->{vmid};
467 delete $job->{pool};
468 } elsif ($job->{pool}) {
469 delete $job->{vmid};
470 delete $job->{all};
471 delete $job->{exclude};
472 }
ac27b58d 473
305921b1 474 PVE::VZDump::verify_vzdump_parameters($job, 1);
ac27b58d 475
305921b1
DC
476 if ($schedule_updated) {
477 PVE::Jobs::updated_job_schedule($id, 'vzdump');
478 }
ac27b58d 479
305921b1
DC
480 if (defined($idx)) {
481 cfs_write_file('vzdump.cron', $data);
ac27b58d 482 }
305921b1
DC
483 cfs_write_file('jobs.cfg', $jobs_data);
484 return;
200cef80 485 };
305921b1
DC
486 cfs_lock_file('vzdump.cron', undef, sub {
487 cfs_lock_file('jobs.cfg', undef, $update_job);
488 die "$@" if ($@);
489 });
200cef80 490 die "$@" if ($@);
ac27b58d
DM
491 }});
492
ac0fe8b6
AL
493__PACKAGE__->register_method({
494 name => 'get_volume_backup_included',
495 path => '{id}/included_volumes',
496 method => 'GET',
497 protected => 1,
498 description => "Returns included guests and the backup status of their disks. Optimized to be used in ExtJS tree views.",
499 permissions => {
500 check => ['perm', '/', ['Sys.Audit']],
501 },
502 parameters => {
503 additionalProperties => 0,
504 properties => {
505 id => $vzdump_job_id_prop
506 },
507 },
508 returns => {
509 type => 'object',
510 description => 'Root node of the tree object. Children represent guests, grandchildren represent volumes of that guest.',
511 properties => {
512 children => {
513 type => 'array',
514 items => {
515 type => 'object',
516 properties => {
517 id => {
518 type => 'integer',
519 description => 'VMID of the guest.',
520 },
521 name => {
522 type => 'string',
523 description => 'Name of the guest',
524 optional => 1,
525 },
526 type => {
527 type => 'string',
528 description => 'Type of the guest, VM, CT or unknown for removed but not purged guests.',
529 enum => ['qemu', 'lxc', 'unknown'],
530 },
531 children => {
532 type => 'array',
533 optional => 1,
534 description => 'The volumes of the guest with the information if they will be included in backups.',
535 items => {
536 type => 'object',
537 properties => {
538 id => {
539 type => 'string',
540 description => 'Configuration key of the volume.',
541 },
542 name => {
543 type => 'string',
544 description => 'Name of the volume.',
545 },
546 included => {
547 type => 'boolean',
548 description => 'Whether the volume is included in the backup or not.',
549 },
550 reason => {
551 type => 'string',
552 description => 'The reason why the volume is included (or excluded).',
553 },
554 },
555 },
556 },
557 },
558 },
559 },
560 },
561 },
562 code => sub {
563 my ($param) = @_;
564
565 my $rpcenv = PVE::RPCEnvironment::get();
566
567 my $user = $rpcenv->get_user();
568
569 my $vzconf = cfs_read_file('vzdump.cron');
570 my $all_jobs = $vzconf->{jobs} || [];
571 my $job;
572 my $rrd = PVE::Cluster::rrd_dump();
573
574 for my $j (@$all_jobs) {
575 if ($j->{id} eq $param->{id}) {
576 $job = $j;
577 last;
578 }
579 }
305921b1
DC
580 if (!$job) {
581 my $jobs_data = cfs_read_file('jobs.cfg');
582 my $j = $jobs_data->{ids}->{$param->{id}};
583 if ($j && $j->{type} eq 'vzdump') {
584 $job = $j;
585 }
586 }
ac0fe8b6
AL
587 raise_param_exc({ id => "No such job '$param->{id}'" }) if !$job;
588
589 my $vmlist = PVE::Cluster::get_vmlist();
590
591 my @job_vmids;
592
593 my $included_guests = PVE::VZDump::get_included_guests($job);
594
595 for my $node (keys %{$included_guests}) {
596 my $node_vmids = $included_guests->{$node};
597 push(@job_vmids, @{$node_vmids});
598 }
599
600 # remove VMIDs to which the user has no permission to not leak infos
601 # like the guest name
602 my @allowed_vmids = grep {
603 $rpcenv->check($user, "/vms/$_", [ 'VM.Audit' ], 1);
604 } @job_vmids;
605
606 my $result = {
607 children => [],
608 };
609
610 for my $vmid (@allowed_vmids) {
611
612 my $children = [];
613
614 # It's possible that a job has VMIDs configured that are not in
615 # vmlist. This could be because a guest was removed but not purged.
616 # Since there is no more data available we can only deliver the VMID
617 # and no volumes.
618 if (!defined $vmlist->{ids}->{$vmid}) {
619 push(@{$result->{children}}, {
620 id => int($vmid),
621 type => 'unknown',
622 leaf => 1,
623 });
624 next;
625 }
626
627 my $type = $vmlist->{ids}->{$vmid}->{type};
628 my $node = $vmlist->{ids}->{$vmid}->{node};
629
630 my $conf;
631 my $volumes;
632 my $name = "";
633
634 if ($type eq 'qemu') {
635 $conf = PVE::QemuConfig->load_config($vmid, $node);
636 $volumes = PVE::QemuConfig->get_backup_volumes($conf);
637 $name = $conf->{name};
638 } elsif ($type eq 'lxc') {
639 $conf = PVE::LXC::Config->load_config($vmid, $node);
640 $volumes = PVE::LXC::Config->get_backup_volumes($conf);
641 $name = $conf->{hostname};
642 } else {
643 die "VMID $vmid is neither Qemu nor LXC guest\n";
644 }
645
646 foreach my $volume (@$volumes) {
647 my $disk = {
648 # id field must be unique for ExtJS tree view
649 id => "$vmid:$volume->{key}",
650 name => $volume->{volume_config}->{file} // $volume->{volume_config}->{volume},
651 included=> $volume->{included},
652 reason => $volume->{reason},
653 leaf => 1,
654 };
655 push(@{$children}, $disk);
656 }
657
658 my $leaf = 0;
659 # it's possible for a guest to have no volumes configured
660 $leaf = 1 if !@{$children};
661
662 push(@{$result->{children}}, {
663 id => int($vmid),
664 type => $type,
665 name => $name,
666 children => $children,
667 leaf => $leaf,
668 });
669 }
670
671 return $result;
672 }});
673
ac27b58d 6741;