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