]> git.proxmox.com Git - pve-manager.git/blame - PVE/API2/Backup.pm
update shipped aplinfo
[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 },
c61c192e
DC
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 },
998b61fb
DC
190 comment => {
191 optional => 1,
192 type => 'string',
193 description => "Description for the Job.",
194 maxLength => 512,
195 },
ac27b58d
DM
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
2617768f 205 $assert_param_permission->($param, $user);
f0bbc084 206
c92c54d5
TL
207 if (my $pool = $param->{pool}) {
208 $rpcenv->check_pool_exist($pool);
209 $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
210 }
211
81ba0803 212 $schedule_param_check->($param, 1);
c92c54d5 213
305921b1
DC
214 $param->{enabled} = 1 if !defined($param->{enabled});
215
216 # autogenerate id for api compatibility FIXME remove with 8.0
ca62f164 217 my $id = extract_param($param, 'id') // UUID::uuid();
305921b1
DC
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};
ac27b58d 224
200cef80 225 PVE::VZDump::verify_vzdump_parameters($param, 1);
305921b1
DC
226 my $plugin = PVE::Jobs::Plugin->lookup('vzdump');
227 my $opts = $plugin->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);
305921b1
DC
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 }
ac27b58d 456
c61c192e
DC
457 my $deletable = {
458 comment => 1,
459 'repeat-missed' => 1,
460 };
461
305921b1 462 foreach my $k (@$delete) {
c61c192e 463 if (!PVE::VZDump::option_exists($k) && !$deletable->{$k}) {
305921b1
DC
464 raise_param_exc({ delete => "unknown option '$k'" });
465 }
ac27b58d 466
305921b1
DC
467 delete $job->{$k};
468 }
53c6bb6c 469
305921b1
DC
470 foreach my $k (keys %$param) {
471 $job->{$k} = $param->{$k};
472 }
ac27b58d 473
305921b1
DC
474 $job->{all} = 1 if (defined($job->{exclude}) && !defined($job->{pool}));
475
476 if (defined($param->{vmid})) {
477 delete $job->{all};
478 delete $job->{exclude};
479 delete $job->{pool};
480 } elsif ($param->{all}) {
481 delete $job->{vmid};
482 delete $job->{pool};
483 } elsif ($job->{pool}) {
484 delete $job->{vmid};
485 delete $job->{all};
486 delete $job->{exclude};
487 }
ac27b58d 488
305921b1 489 PVE::VZDump::verify_vzdump_parameters($job, 1);
ac27b58d 490
305921b1
DC
491 if (defined($idx)) {
492 cfs_write_file('vzdump.cron', $data);
ac27b58d 493 }
305921b1 494 cfs_write_file('jobs.cfg', $jobs_data);
2cf7706e
DC
495
496 PVE::Jobs::detect_changed_runtime_props($id, 'vzdump', $job);
497
305921b1 498 return;
200cef80 499 };
305921b1
DC
500 cfs_lock_file('vzdump.cron', undef, sub {
501 cfs_lock_file('jobs.cfg', undef, $update_job);
502 die "$@" if ($@);
503 });
200cef80 504 die "$@" if ($@);
ac27b58d
DM
505 }});
506
ac0fe8b6
AL
507__PACKAGE__->register_method({
508 name => 'get_volume_backup_included',
509 path => '{id}/included_volumes',
510 method => 'GET',
511 protected => 1,
512 description => "Returns included guests and the backup status of their disks. Optimized to be used in ExtJS tree views.",
513 permissions => {
514 check => ['perm', '/', ['Sys.Audit']],
515 },
516 parameters => {
517 additionalProperties => 0,
518 properties => {
519 id => $vzdump_job_id_prop
520 },
521 },
522 returns => {
523 type => 'object',
524 description => 'Root node of the tree object. Children represent guests, grandchildren represent volumes of that guest.',
525 properties => {
526 children => {
527 type => 'array',
528 items => {
529 type => 'object',
530 properties => {
531 id => {
532 type => 'integer',
533 description => 'VMID of the guest.',
534 },
535 name => {
536 type => 'string',
537 description => 'Name of the guest',
538 optional => 1,
539 },
540 type => {
541 type => 'string',
542 description => 'Type of the guest, VM, CT or unknown for removed but not purged guests.',
543 enum => ['qemu', 'lxc', 'unknown'],
544 },
545 children => {
546 type => 'array',
547 optional => 1,
548 description => 'The volumes of the guest with the information if they will be included in backups.',
549 items => {
550 type => 'object',
551 properties => {
552 id => {
553 type => 'string',
554 description => 'Configuration key of the volume.',
555 },
556 name => {
557 type => 'string',
558 description => 'Name of the volume.',
559 },
560 included => {
561 type => 'boolean',
562 description => 'Whether the volume is included in the backup or not.',
563 },
564 reason => {
565 type => 'string',
566 description => 'The reason why the volume is included (or excluded).',
567 },
568 },
569 },
570 },
571 },
572 },
573 },
574 },
575 },
576 code => sub {
577 my ($param) = @_;
578
579 my $rpcenv = PVE::RPCEnvironment::get();
580
581 my $user = $rpcenv->get_user();
582
583 my $vzconf = cfs_read_file('vzdump.cron');
584 my $all_jobs = $vzconf->{jobs} || [];
585 my $job;
586 my $rrd = PVE::Cluster::rrd_dump();
587
588 for my $j (@$all_jobs) {
589 if ($j->{id} eq $param->{id}) {
590 $job = $j;
591 last;
592 }
593 }
305921b1
DC
594 if (!$job) {
595 my $jobs_data = cfs_read_file('jobs.cfg');
596 my $j = $jobs_data->{ids}->{$param->{id}};
597 if ($j && $j->{type} eq 'vzdump') {
598 $job = $j;
599 }
600 }
ac0fe8b6
AL
601 raise_param_exc({ id => "No such job '$param->{id}'" }) if !$job;
602
603 my $vmlist = PVE::Cluster::get_vmlist();
604
605 my @job_vmids;
606
607 my $included_guests = PVE::VZDump::get_included_guests($job);
608
609 for my $node (keys %{$included_guests}) {
610 my $node_vmids = $included_guests->{$node};
611 push(@job_vmids, @{$node_vmids});
612 }
613
614 # remove VMIDs to which the user has no permission to not leak infos
615 # like the guest name
616 my @allowed_vmids = grep {
617 $rpcenv->check($user, "/vms/$_", [ 'VM.Audit' ], 1);
618 } @job_vmids;
619
620 my $result = {
621 children => [],
622 };
623
624 for my $vmid (@allowed_vmids) {
625
626 my $children = [];
627
628 # It's possible that a job has VMIDs configured that are not in
629 # vmlist. This could be because a guest was removed but not purged.
630 # Since there is no more data available we can only deliver the VMID
631 # and no volumes.
632 if (!defined $vmlist->{ids}->{$vmid}) {
633 push(@{$result->{children}}, {
634 id => int($vmid),
635 type => 'unknown',
636 leaf => 1,
637 });
638 next;
639 }
640
641 my $type = $vmlist->{ids}->{$vmid}->{type};
642 my $node = $vmlist->{ids}->{$vmid}->{node};
643
644 my $conf;
645 my $volumes;
646 my $name = "";
647
648 if ($type eq 'qemu') {
649 $conf = PVE::QemuConfig->load_config($vmid, $node);
650 $volumes = PVE::QemuConfig->get_backup_volumes($conf);
651 $name = $conf->{name};
652 } elsif ($type eq 'lxc') {
653 $conf = PVE::LXC::Config->load_config($vmid, $node);
654 $volumes = PVE::LXC::Config->get_backup_volumes($conf);
655 $name = $conf->{hostname};
656 } else {
657 die "VMID $vmid is neither Qemu nor LXC guest\n";
658 }
659
660 foreach my $volume (@$volumes) {
661 my $disk = {
662 # id field must be unique for ExtJS tree view
663 id => "$vmid:$volume->{key}",
664 name => $volume->{volume_config}->{file} // $volume->{volume_config}->{volume},
665 included=> $volume->{included},
666 reason => $volume->{reason},
667 leaf => 1,
668 };
669 push(@{$children}, $disk);
670 }
671
672 my $leaf = 0;
673 # it's possible for a guest to have no volumes configured
674 $leaf = 1 if !@{$children};
675
676 push(@{$result->{children}}, {
677 id => int($vmid),
678 type => $type,
679 name => $name,
680 children => $children,
681 leaf => $leaf,
682 });
683 }
684
685 return $result;
686 }});
687
ac27b58d 6881;