]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Backup.pm
Code cleanup in Backup
[pve-manager.git] / PVE / API2 / Backup.pm
1 package PVE::API2::Backup;
2
3 use strict;
4 use warnings;
5 use Digest::SHA;
6
7 use PVE::SafeSyslog;
8 use PVE::Tools qw(extract_param);
9 use PVE::Cluster qw(cfs_register_file cfs_lock_file cfs_read_file cfs_write_file);
10 use PVE::RESTHandler;
11 use PVE::RPCEnvironment;
12 use PVE::JSONSchema;
13 use PVE::Storage;
14 use PVE::Exception qw(raise_param_exc);
15 use PVE::VZDump;
16
17 use base qw(PVE::RESTHandler);
18
19 cfs_register_file ('vzdump.cron',
20 \&parse_vzdump_cron_config,
21 \&write_vzdump_cron_config);
22
23 PVE::JSONSchema::register_format('pve-day-of-week', \&verify_day_of_week);
24 sub verify_day_of_week {
25 my ($value, $noerr) = @_;
26
27 return $value if $value =~ m/^(mon|tue|wed|thu|fri|sat|sun)$/;
28
29 return undef if $noerr;
30
31 die "invalid day '$value'\n";
32 }
33
34 my $vzdump_job_id_prop = {
35 type => 'string',
36 description => "The job ID.",
37 maxLength => 50
38 };
39
40 my $dowhash_to_dow = sub {
41 my ($d, $num) = @_;
42
43 my @da = ();
44 push @da, $num ? 1 : 'mon' if $d->{mon};
45 push @da, $num ? 2 : 'tue' if $d->{tue};
46 push @da, $num ? 3 : 'wed' if $d->{wed};
47 push @da, $num ? 4 : 'thu' if $d->{thu};
48 push @da, $num ? 5 : 'fri' if $d->{fri};
49 push @da, $num ? 6 : 'sat' if $d->{sat};
50 push @da, $num ? 7 : 'sun' if $d->{sun};
51
52 return join ',', @da;
53 };
54
55 # parse crontab style day of week
56 sub parse_dow {
57 my ($dowstr, $noerr) = @_;
58
59 my $dowmap = {mon => 1, tue => 2, wed => 3, thu => 4,
60 fri => 5, sat => 6, sun => 7};
61 my $rdowmap = { '1' => 'mon', '2' => 'tue', '3' => 'wed', '4' => 'thu',
62 '5' => 'fri', '6' => 'sat', '7' => 'sun', '0' => 'sun'};
63
64 my $res = {};
65
66 $dowstr = '1,2,3,4,5,6,7' if $dowstr eq '*';
67
68 foreach my $day (PVE::Tools::split_list($dowstr)) {
69 if ($day =~ m/^(mon|tue|wed|thu|fri|sat|sun)-(mon|tue|wed|thu|fri|sat|sun)$/i) {
70 for (my $i = $dowmap->{lc($1)}; $i <= $dowmap->{lc($2)}; $i++) {
71 my $r = $rdowmap->{$i};
72 $res->{$r} = 1;
73 }
74 } elsif ($day =~ m/^(mon|tue|wed|thu|fri|sat|sun|[0-7])$/i) {
75 $day = $rdowmap->{$day} if $day =~ m/\d/;
76 $res->{lc($day)} = 1;
77 } else {
78 return undef if $noerr;
79 die "unable to parse day of week '$dowstr'\n";
80 }
81 }
82
83 return $res;
84 };
85
86 my $vzdump_properties = {
87 additionalProperties => 0,
88 properties => PVE::VZDump::json_config_properties({}),
89 };
90
91 sub parse_vzdump_cron_config {
92 my ($filename, $raw) = @_;
93
94 my $jobs = []; # correct jobs
95
96 my $ejobs = []; # mailfomerd lines
97
98 my $jid = 1; # we start at 1
99
100 my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : '');
101
102 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
103 my $line = $1;
104
105 next if $line =~ m/^\#/;
106 next if $line =~ m/^\s*$/;
107 next if $line =~ m/^PATH\s*=/; # we always overwrite path
108
109 if ($line =~ m|^(\d+)\s+(\d+)\s+\*\s+\*\s+(\S+)\s+root\s+(/\S+/)?(#)?vzdump(\s+(.*))?$|) {
110 eval {
111 my $minute = int($1);
112 my $hour = int($2);
113 my $dow = $3;
114 my $param = $7;
115 my $enabled = $5;
116
117 my $dowhash = parse_dow($dow, 1);
118 die "unable to parse day of week '$dow' in '$filename'\n" if !$dowhash;
119
120 my $args = PVE::Tools::split_args($param);
121 my $opts = PVE::JSONSchema::get_options($vzdump_properties, $args, 'vmid');
122
123 $opts->{enabled} = !defined($enabled);
124 $opts->{id} = "$digest:$jid";
125 $jid++;
126 $opts->{starttime} = sprintf "%02d:%02d", $hour, $minute;
127 $opts->{dow} = &$dowhash_to_dow($dowhash);
128
129 push @$jobs, $opts;
130 };
131 my $err = $@;
132 if ($err) {
133 syslog ('err', "parse error in '$filename': $err");
134 push @$ejobs, { line => $line };
135 }
136 } elsif ($line =~ m|^\S+\s+(\S+)\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S.*)$|) {
137 syslog ('err', "warning: malformed line in '$filename'");
138 push @$ejobs, { line => $line };
139 } else {
140 syslog ('err', "ignoring malformed line in '$filename'");
141 }
142 }
143
144 my $res = {};
145 $res->{digest} = $digest;
146 $res->{jobs} = $jobs;
147 $res->{ejobs} = $ejobs;
148
149 return $res;
150 }
151
152 sub write_vzdump_cron_config {
153 my ($filename, $cfg) = @_;
154
155 my $out = "# cluster wide vzdump cron schedule\n";
156 $out .= "# Automatically generated file - do not edit\n\n";
157 $out .= "PATH=\"/usr/sbin:/usr/bin:/sbin:/bin\"\n\n";
158
159 my $jobs = $cfg->{jobs} || [];
160 foreach my $job (@$jobs) {
161 my $enabled = ($job->{enabled}) ? '' : '#';
162 my $dh = parse_dow($job->{dow});
163 my $dow;
164 if ($dh->{mon} && $dh->{tue} && $dh->{wed} && $dh->{thu} &&
165 $dh->{fri} && $dh->{sat} && $dh->{sun}) {
166 $dow = '*';
167 } else {
168 $dow = &$dowhash_to_dow($dh, 1);
169 $dow = '*' if !$dow;
170 }
171
172 my ($hour, $minute);
173
174 die "no job start time specified\n" if !$job->{starttime};
175 if ($job->{starttime} =~ m/^(\d{1,2}):(\d{1,2})$/) {
176 ($hour, $minute) = (int($1), int($2));
177 die "hour '$hour' out of range\n" if $hour < 0 || $hour > 23;
178 die "minute '$minute' out of range\n" if $minute < 0 || $minute > 59;
179 } else {
180 die "unable to parse job start time\n";
181 }
182
183 $job->{quiet} = 1; # we do not want messages from cron
184
185 my $cmd = PVE::VZDump::command_line($job);
186
187 $out .= sprintf "$minute $hour * * %-11s root $enabled$cmd\n", $dow;
188 }
189
190 my $ejobs = $cfg->{ejobs} || [];
191 foreach my $job (@$ejobs) {
192 $out .= "$job->{line}\n" if $job->{line};
193 }
194
195 return $out;
196 }
197
198 __PACKAGE__->register_method({
199 name => 'index',
200 path => '',
201 method => 'GET',
202 description => "List vzdump backup schedule.",
203 permissions => {
204 check => ['perm', '/', ['Sys.Audit']],
205 },
206 parameters => {
207 additionalProperties => 0,
208 properties => {},
209 },
210 returns => {
211 type => 'array',
212 items => {
213 type => "object",
214 properties => {
215 id => $vzdump_job_id_prop
216 },
217 },
218 links => [ { rel => 'child', href => "{id}" } ],
219 },
220 code => sub {
221 my ($param) = @_;
222
223 my $rpcenv = PVE::RPCEnvironment::get();
224 my $user = $rpcenv->get_user();
225
226 my $data = cfs_read_file('vzdump.cron');
227
228 my $res = $data->{jobs} || [];
229
230 return $res;
231 }});
232
233 __PACKAGE__->register_method({
234 name => 'create_job',
235 path => '',
236 method => 'POST',
237 protected => 1,
238 description => "Create new vzdump backup job.",
239 permissions => {
240 check => ['perm', '/', ['Sys.Modify']],
241 description => "The 'tmpdir', 'dumpdir' and 'script' parameters are additionally restricted to the 'root\@pam' user.",
242 },
243 parameters => {
244 additionalProperties => 0,
245 properties => PVE::VZDump::json_config_properties({
246 starttime => {
247 type => 'string',
248 description => "Job Start time.",
249 pattern => '\d{1,2}:\d{1,2}',
250 typetext => 'HH:MM',
251 },
252 dow => {
253 type => 'string', format => 'pve-day-of-week-list',
254 optional => 1,
255 description => "Day of week selection.",
256 default => 'mon,tue,wed,thu,fri,sat,sun',
257 },
258 enabled => {
259 type => 'boolean',
260 optional => 1,
261 description => "Enable or disable the job.",
262 default => '1',
263 },
264 }),
265 },
266 returns => { type => 'null' },
267 code => sub {
268 my ($param) = @_;
269
270 my $rpcenv = PVE::RPCEnvironment::get();
271 my $user = $rpcenv->get_user();
272
273 foreach my $key (qw(tmpdir dumpdir script)) {
274 raise_param_exc({ $key => "Only root may set this option."})
275 if defined($param->{$key}) && ($user ne 'root@pam');
276 }
277
278 if (my $pool = $param->{pool}) {
279 $rpcenv->check_pool_exist($pool);
280 $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
281 }
282
283
284 my $create_job = sub {
285 my $data = cfs_read_file('vzdump.cron');
286
287 $param->{dow} = 'mon,tue,wed,thu,fri,sat,sun' if !defined($param->{dow});
288 $param->{enabled} = 1 if !defined($param->{enabled});
289 PVE::VZDump::verify_vzdump_parameters($param, 1);
290
291 push @{$data->{jobs}}, $param;
292
293 cfs_write_file('vzdump.cron', $data);
294 };
295 cfs_lock_file('vzdump.cron', undef, $create_job);
296 die "$@" if ($@);
297
298 return undef;
299 }});
300
301 __PACKAGE__->register_method({
302 name => 'read_job',
303 path => '{id}',
304 method => 'GET',
305 description => "Read vzdump backup job definition.",
306 permissions => {
307 check => ['perm', '/', ['Sys.Audit']],
308 },
309 parameters => {
310 additionalProperties => 0,
311 properties => {
312 id => $vzdump_job_id_prop
313 },
314 },
315 returns => {
316 type => 'object',
317 },
318 code => sub {
319 my ($param) = @_;
320
321 my $rpcenv = PVE::RPCEnvironment::get();
322 my $user = $rpcenv->get_user();
323
324 my $data = cfs_read_file('vzdump.cron');
325
326 my $jobs = $data->{jobs} || [];
327
328 foreach my $job (@$jobs) {
329 return $job if $job->{id} eq $param->{id};
330 }
331
332 raise_param_exc({ id => "No such job '$param->{id}'" });
333
334 }});
335
336 __PACKAGE__->register_method({
337 name => 'delete_job',
338 path => '{id}',
339 method => 'DELETE',
340 description => "Delete vzdump backup job definition.",
341 permissions => {
342 check => ['perm', '/', ['Sys.Modify']],
343 },
344 protected => 1,
345 parameters => {
346 additionalProperties => 0,
347 properties => {
348 id => $vzdump_job_id_prop
349 },
350 },
351 returns => { type => 'null' },
352 code => sub {
353 my ($param) = @_;
354
355 my $rpcenv = PVE::RPCEnvironment::get();
356 my $user = $rpcenv->get_user();
357
358 my $delete_job = sub {
359 my $data = cfs_read_file('vzdump.cron');
360
361 my $jobs = $data->{jobs} || [];
362 my $newjobs = [];
363
364 my $found;
365 foreach my $job (@$jobs) {
366 if ($job->{id} eq $param->{id}) {
367 $found = 1;
368 } else {
369 push @$newjobs, $job;
370 }
371 }
372
373 raise_param_exc({ id => "No such job '$param->{id}'" }) if !$found;
374
375 $data->{jobs} = $newjobs;
376
377 cfs_write_file('vzdump.cron', $data);
378 };
379 cfs_lock_file('vzdump.cron', undef, $delete_job);
380 die "$@" if ($@);
381
382 return undef;
383 }});
384
385 __PACKAGE__->register_method({
386 name => 'update_job',
387 path => '{id}',
388 method => 'PUT',
389 protected => 1,
390 description => "Update vzdump backup job definition.",
391 permissions => {
392 check => ['perm', '/', ['Sys.Modify']],
393 },
394 parameters => {
395 additionalProperties => 0,
396 properties => PVE::VZDump::json_config_properties({
397 id => $vzdump_job_id_prop,
398 starttime => {
399 type => 'string',
400 description => "Job Start time.",
401 pattern => '\d{1,2}:\d{1,2}',
402 typetext => 'HH:MM',
403 },
404 dow => {
405 type => 'string', format => 'pve-day-of-week-list',
406 optional => 1,
407 description => "Day of week selection.",
408 },
409 delete => {
410 type => 'string', format => 'pve-configid-list',
411 description => "A list of settings you want to delete.",
412 optional => 1,
413 },
414 enabled => {
415 type => 'boolean',
416 optional => 1,
417 description => "Enable or disable the job.",
418 default => '1',
419 },
420 }),
421 },
422 returns => { type => 'null' },
423 code => sub {
424 my ($param) = @_;
425
426 my $rpcenv = PVE::RPCEnvironment::get();
427 my $user = $rpcenv->get_user();
428
429 if (my $pool = $param->{pool}) {
430 $rpcenv->check_pool_exist($pool);
431 $rpcenv->check($user, "/pool/$pool", ['VM.Backup']);
432 }
433
434 my $update_job = sub {
435 my $data = cfs_read_file('vzdump.cron');
436
437 my $jobs = $data->{jobs} || [];
438
439 die "no options specified\n" if !scalar(keys %$param);
440
441 PVE::VZDump::verify_vzdump_parameters($param);
442
443 my @delete = PVE::Tools::split_list(extract_param($param, 'delete'));
444
445 foreach my $job (@$jobs) {
446 if ($job->{id} eq $param->{id}) {
447
448 foreach my $k (@delete) {
449 if (!PVE::VZDump::option_exists($k)) {
450 raise_param_exc({ delete => "unknown option '$k'" });
451 }
452
453 delete $job->{$k};
454 }
455
456 foreach my $k (keys %$param) {
457 $job->{$k} = $param->{$k};
458 }
459
460 $job->{all} = 1 if (defined($job->{exclude}) && !defined($job->{pool}));
461
462 if (defined($param->{vmid})) {
463 delete $job->{all};
464 delete $job->{exclude};
465 delete $job->{pool};
466 } elsif ($param->{all}) {
467 delete $job->{vmid};
468 delete $job->{pool};
469 } elsif ($job->{pool}) {
470 delete $job->{vmid};
471 delete $job->{all};
472 }
473
474 PVE::VZDump::verify_vzdump_parameters($job, 1);
475
476 cfs_write_file('vzdump.cron', $data);
477
478 return undef;
479 }
480 }
481 raise_param_exc({ id => "No such job '$param->{id}'" });
482 };
483 cfs_lock_file('vzdump.cron', undef, $update_job);
484 die "$@" if ($@);
485 }});
486
487 1;