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