]> git.proxmox.com Git - pve-manager.git/blame - PVE/API2/Backup.pm
add vzdump-hook-script.pl script to examples
[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
18cfs_register_file ('vzdump',
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);
ac27b58d
DM
114 my $opts = PVE::JSONSchema::get_options($vzdump_propetries, $args, undef, undef, 'vmid');
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 }
173
ac27b58d
DM
174 my $param = "";
175 foreach my $p (keys %$job) {
7625ea19 176 next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || $p eq 'dow';
063bb19b
DM
177 my $v = $job->{$p};
178 $param .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne '';
ac27b58d
DM
179 }
180
53c6bb6c
DM
181 if ($job->{vmid}) {
182 $param .= " " . join(' ', PVE::Tools::split_list($job->{vmid}));
183 }
ac27b58d 184
7625ea19 185 $out .= sprintf "$minute $hour * * %-11s root vzdump$param\n", $dow;
ac27b58d
DM
186 }
187
188 my $ejobs = $cfg->{ejobs} || [];
189 foreach my $job (@$ejobs) {
190 $out .= "$job->{line}\n" if $job->{line};
191 }
192
193 return $out;
194}
195
196__PACKAGE__->register_method({
197 name => 'index',
198 path => '',
199 method => 'GET',
200 description => "List vzdump backup schedule.",
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');
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 parameters => {
235 additionalProperties => 0,
236 properties => PVE::VZDump::json_config_properties({
7625ea19
DM
237 starttime => {
238 type => 'string',
239 description => "Job Start time.",
240 pattern => '\d{1,2}:\d{1,2}',
241 typetext => 'HH:MM',
ac27b58d
DM
242 },
243 dow => {
244 type => 'string', format => 'pve-day-of-week-list',
245 optional => 1,
246 description => "Day of week selection.",
247 default => 'mon,tue,wed,thu,fri,sat,sun',
248 },
249 }),
250 },
251 returns => { type => 'null' },
252 code => sub {
253 my ($param) = @_;
254
255 my $rpcenv = PVE::RPCEnvironment::get();
256 my $user = $rpcenv->get_user();
257
258 my $data = cfs_read_file('vzdump');
259
ac27b58d
DM
260 $param->{dow} = 'mon,tue,wed,thu,fri,sat,sun' if !defined($param->{dow});
261
262 $param->{all} = 1 if defined($param->{exclude});
263 raise_param_exc({ all => "option conflicts with option 'vmid'"})
264 if $param->{all} && $param->{vmid};
265
266 raise_param_exc({ vmid => "property is missing"})
267 if !$param->{all} && !$param->{vmid};
268
269 push @{$data->{jobs}}, $param;
270
271 cfs_write_file('vzdump', $data);
272
273 return undef;
274 }});
275
276__PACKAGE__->register_method({
277 name => 'read_job',
278 path => '{id}',
279 method => 'GET',
280 description => "Read vzdump backup job definition.",
281 parameters => {
282 additionalProperties => 0,
283 properties => {
284 id => {
285 type => 'string',
286 description => "The job ID.",
287 maxLength => 50,
288 }
289 },
290 },
291 returns => {
292 type => 'object',
293 },
294 code => sub {
295 my ($param) = @_;
296
297 my $rpcenv = PVE::RPCEnvironment::get();
298 my $user = $rpcenv->get_user();
299
300 my $data = cfs_read_file('vzdump');
301
302 my $jobs = $data->{jobs} || [];
303
304 foreach my $job (@$jobs) {
305 return $job if $job->{id} eq $param->{id};
306 }
307
308 raise_param_exc({ id => "No such job '$param->{id}'" });
309
310 }});
311
312__PACKAGE__->register_method({
313 name => 'delete_job',
314 path => '{id}',
315 method => 'DELETE',
316 description => "Delete vzdump backup job definition.",
317 protected => 1,
318 parameters => {
319 additionalProperties => 0,
320 properties => {
321 id => {
322 type => 'string',
323 description => "The job ID.",
324 maxLength => 50,
325 }
326 },
327 },
328 returns => { type => 'null' },
329 code => sub {
330 my ($param) = @_;
331
332 my $rpcenv = PVE::RPCEnvironment::get();
333 my $user = $rpcenv->get_user();
334
335 my $data = cfs_read_file('vzdump');
336
337 my $jobs = $data->{jobs} || [];
338 my $newjobs = [];
339
340 my $found;
341 foreach my $job (@$jobs) {
342 if ($job->{id} eq $param->{id}) {
343 $found = 1;
344 } else {
345 push @$newjobs, $job;
346 }
347 }
348
349 raise_param_exc({ id => "No such job '$param->{id}'" }) if !$found;
350
351 $data->{jobs} = $newjobs;
352
353 cfs_write_file('vzdump', $data);
354
355 return undef;
356 }});
357
358__PACKAGE__->register_method({
359 name => 'update_job',
360 path => '{id}',
361 method => 'PUT',
362 protected => 1,
363 description => "Update vzdump backup job definition.",
364 parameters => {
365 additionalProperties => 0,
366 properties => PVE::VZDump::json_config_properties({
367 id => {
368 type => 'string',
369 description => "The job ID.",
370 maxLength => 50,
371 },
7625ea19
DM
372 starttime => {
373 type => 'string',
374 description => "Job Start time.",
375 pattern => '\d{1,2}:\d{1,2}',
376 typetext => 'HH:MM',
ac27b58d
DM
377 },
378 dow => {
379 type => 'string', format => 'pve-day-of-week-list',
380 optional => 1,
381 description => "Day of week selection.",
382 },
53c6bb6c
DM
383 delete => {
384 type => 'string', format => 'pve-configid-list',
385 description => "A list of settings you want to delete.",
386 optional => 1,
387 },
ac27b58d
DM
388 }),
389 },
390 returns => { type => 'null' },
391 code => sub {
392 my ($param) = @_;
393
394 my $rpcenv = PVE::RPCEnvironment::get();
395 my $user = $rpcenv->get_user();
396
397 my $data = cfs_read_file('vzdump');
398
399 my $jobs = $data->{jobs} || [];
400
53c6bb6c
DM
401 die "no options specified\n" if !scalar(keys %$param);
402
ac27b58d
DM
403 raise_param_exc({ all => "option conflicts with option 'vmid'"})
404 if $param->{all} && $param->{vmid};
405
53c6bb6c
DM
406 my $delete = extract_param($param, 'delete');
407
ac27b58d
DM
408 foreach my $job (@$jobs) {
409 if ($job->{id} eq $param->{id}) {
410
53c6bb6c
DM
411 foreach my $k (PVE::Tools::split_list($delete)) {
412 if (!PVE::VZDump::option_exists($k)) {
413 raise_param_exc({ delete => "unknown option '$k'" });
414 }
415
416 delete $job->{$k};
417 }
418
ac27b58d
DM
419 foreach my $k (keys %$param) {
420 $job->{$k} = $param->{$k};
421 }
422
423 $job->{all} = 1 if defined($job->{exclude});
424
425 if ($param->{vmid}) {
426 delete $job->{all};
427 delete $job->{exclude};
428 } elsif ($param->{all}) {
429 delete $job->{vmid};
430 }
431
432 raise_param_exc({ all => "option conflicts with option 'vmid'"})
433 if $job->{all} && $job->{vmid};
434
435 raise_param_exc({ vmid => "property is missing"})
436 if !$job->{all} && !$job->{vmid};
437
438 cfs_write_file('vzdump', $data);
439
440 return undef;
441 }
442 }
443
444 raise_param_exc({ id => "No such job '$param->{id}'" });
445
446 }});
447
4481;