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