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