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