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