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