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