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