]>
Commit | Line | Data |
---|---|---|
e2a0a93b CE |
1 | package PVE::VZDump::Common; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use Digest::SHA; | |
6 | ||
7 | use PVE::Tools; | |
8 | use PVE::SafeSyslog qw(syslog); | |
9 | use PVE::Storage; | |
10 | use PVE::Cluster qw(cfs_register_file); | |
11 | use PVE::JSONSchema qw(get_standard_option); | |
12 | ||
13 | cfs_register_file('vzdump.cron', | |
14 | \&parse_vzdump_cron_config, | |
15 | \&write_vzdump_cron_config); | |
16 | ||
17 | my $dowhash_to_dow = sub { | |
18 | my ($d, $num) = @_; | |
19 | ||
20 | my @da = (); | |
21 | push @da, $num ? 1 : 'mon' if $d->{mon}; | |
22 | push @da, $num ? 2 : 'tue' if $d->{tue}; | |
23 | push @da, $num ? 3 : 'wed' if $d->{wed}; | |
24 | push @da, $num ? 4 : 'thu' if $d->{thu}; | |
25 | push @da, $num ? 5 : 'fri' if $d->{fri}; | |
26 | push @da, $num ? 6 : 'sat' if $d->{sat}; | |
27 | push @da, $num ? 7 : 'sun' if $d->{sun}; | |
28 | ||
29 | return join ',', @da; | |
30 | }; | |
31 | ||
533d6e50 FE |
32 | my $fixup_prune_backups_option = sub { |
33 | my ($opts) = @_; | |
34 | ||
35 | return if !defined($opts->{'prune-backups'}); | |
36 | ||
37 | $opts->{'prune-backups'} = PVE::JSONSchema::parse_property_string( | |
38 | 'prune-backups', | |
39 | $opts->{'prune-backups'} | |
40 | ); | |
41 | }; | |
42 | ||
e2a0a93b CE |
43 | # parse crontab style day of week |
44 | sub parse_dow { | |
45 | my ($dowstr, $noerr) = @_; | |
46 | ||
47 | my $dowmap = {mon => 1, tue => 2, wed => 3, thu => 4, | |
48 | fri => 5, sat => 6, sun => 7}; | |
49 | my $rdowmap = { '1' => 'mon', '2' => 'tue', '3' => 'wed', '4' => 'thu', | |
50 | '5' => 'fri', '6' => 'sat', '7' => 'sun', '0' => 'sun'}; | |
51 | ||
52 | my $res = {}; | |
53 | ||
54 | $dowstr = '1,2,3,4,5,6,7' if $dowstr eq '*'; | |
55 | ||
56 | foreach my $day (PVE::Tools::split_list($dowstr)) { | |
57 | if ($day =~ m/^(mon|tue|wed|thu|fri|sat|sun)-(mon|tue|wed|thu|fri|sat|sun)$/i) { | |
58 | for (my $i = $dowmap->{lc($1)}; $i <= $dowmap->{lc($2)}; $i++) { | |
59 | my $r = $rdowmap->{$i}; | |
60 | $res->{$r} = 1; | |
61 | } | |
62 | } elsif ($day =~ m/^(mon|tue|wed|thu|fri|sat|sun|[0-7])$/i) { | |
63 | $day = $rdowmap->{$day} if $day =~ m/\d/; | |
64 | $res->{lc($day)} = 1; | |
65 | } else { | |
66 | return undef if $noerr; | |
67 | die "unable to parse day of week '$dowstr'\n"; | |
68 | } | |
69 | } | |
70 | ||
71 | return $res; | |
72 | }; | |
73 | ||
74 | my $confdesc = { | |
75 | vmid => { | |
76 | type => 'string', format => 'pve-vmid-list', | |
77 | description => "The ID of the guest system you want to backup.", | |
78 | completion => \&PVE::Cluster::complete_local_vmid, | |
79 | optional => 1, | |
80 | }, | |
81 | node => get_standard_option('pve-node', { | |
82 | description => "Only run if executed on this node.", | |
83 | completion => \&PVE::Cluster::get_nodelist, | |
84 | optional => 1, | |
85 | }), | |
86 | all => { | |
87 | type => 'boolean', | |
88 | description => "Backup all known guest systems on this host.", | |
89 | optional => 1, | |
90 | default => 0, | |
91 | }, | |
92 | stdexcludes => { | |
93 | type => 'boolean', | |
94 | description => "Exclude temporary files and logs.", | |
95 | optional => 1, | |
96 | default => 1, | |
97 | }, | |
98 | compress => { | |
99 | type => 'string', | |
100 | description => "Compress dump file.", | |
101 | optional => 1, | |
e8ca2137 | 102 | enum => ['0', '1', 'gzip', 'lzo', 'zstd'], |
e2a0a93b CE |
103 | default => '0', |
104 | }, | |
105 | pigz=> { | |
106 | type => "integer", | |
107 | description => "Use pigz instead of gzip when N>0.". | |
108 | " N=1 uses half of cores, N>1 uses N as thread count.", | |
109 | optional => 1, | |
110 | default => 0, | |
111 | }, | |
e8ca2137 AA |
112 | zstd => { |
113 | type => "integer", | |
114 | description => "Zstd threads. N=0 uses half of the available cores,". | |
115 | " N>0 uses N as thread count.", | |
116 | optional => 1, | |
117 | default => 1, | |
118 | }, | |
e2a0a93b CE |
119 | quiet => { |
120 | type => 'boolean', | |
121 | description => "Be quiet.", | |
122 | optional => 1, | |
123 | default => 0, | |
124 | }, | |
125 | mode => { | |
126 | type => 'string', | |
127 | description => "Backup mode.", | |
128 | optional => 1, | |
129 | default => 'snapshot', | |
130 | enum => [ 'snapshot', 'suspend', 'stop' ], | |
131 | }, | |
132 | exclude => { | |
133 | type => 'string', format => 'pve-vmid-list', | |
134 | description => "Exclude specified guest systems (assumes --all)", | |
135 | optional => 1, | |
136 | }, | |
137 | 'exclude-path' => { | |
138 | type => 'string', format => 'string-alist', | |
5da50f8c FE |
139 | description => "Exclude certain files/directories (shell globs)." . |
140 | " Paths starting with '/' are anchored to the container's root, " . | |
141 | " other paths match relative to each subdirectory.", | |
e2a0a93b CE |
142 | optional => 1, |
143 | }, | |
144 | mailto => { | |
295a6359 | 145 | type => 'string', |
17b5185b | 146 | format => 'email-or-username-list', |
295a6359 | 147 | description => "Comma-separated list of email addresses or users that should" . |
e2a0a93b CE |
148 | " receive email notifications.", |
149 | optional => 1, | |
150 | }, | |
151 | mailnotification => { | |
152 | type => 'string', | |
153 | description => "Specify when to send an email", | |
154 | optional => 1, | |
155 | enum => [ 'always', 'failure' ], | |
156 | default => 'always', | |
157 | }, | |
158 | tmpdir => { | |
159 | type => 'string', | |
160 | description => "Store temporary files to specified directory.", | |
161 | optional => 1, | |
162 | }, | |
163 | dumpdir => { | |
164 | type => 'string', | |
165 | description => "Store resulting files to specified directory.", | |
166 | optional => 1, | |
167 | }, | |
168 | script => { | |
169 | type => 'string', | |
170 | description => "Use specified hook script.", | |
171 | optional => 1, | |
172 | }, | |
173 | storage => get_standard_option('pve-storage-id', { | |
174 | description => "Store resulting file to this storage.", | |
175 | completion => \&complete_backup_storage, | |
176 | optional => 1, | |
177 | }), | |
178 | stop => { | |
179 | type => 'boolean', | |
180 | description => "Stop running backup jobs on this host.", | |
181 | optional => 1, | |
182 | default => 0, | |
183 | }, | |
0111ceb9 | 184 | # FIXME: Remove with PVE 7.0 |
e2a0a93b CE |
185 | size => { |
186 | type => 'integer', | |
187 | description => "Unused, will be removed in a future release.", | |
188 | optional => 1, | |
189 | minimum => 500, | |
190 | default => 1024, | |
191 | }, | |
192 | bwlimit => { | |
193 | type => 'integer', | |
194 | description => "Limit I/O bandwidth (KBytes per second).", | |
195 | optional => 1, | |
196 | minimum => 0, | |
197 | default => 0, | |
198 | }, | |
199 | ionice => { | |
200 | type => 'integer', | |
201 | description => "Set CFQ ionice priority.", | |
202 | optional => 1, | |
203 | minimum => 0, | |
204 | maximum => 8, | |
205 | default => 7, | |
206 | }, | |
207 | lockwait => { | |
208 | type => 'integer', | |
209 | description => "Maximal time to wait for the global lock (minutes).", | |
210 | optional => 1, | |
211 | minimum => 0, | |
212 | default => 3*60, # 3 hours | |
213 | }, | |
214 | stopwait => { | |
215 | type => 'integer', | |
216 | description => "Maximal time to wait until a guest system is stopped (minutes).", | |
217 | optional => 1, | |
218 | minimum => 0, | |
219 | default => 10, # 10 minutes | |
220 | }, | |
221 | maxfiles => { | |
222 | type => 'integer', | |
223 | description => "Maximal number of backup files per guest system.", | |
224 | optional => 1, | |
225 | minimum => 1, | |
226 | default => 1, | |
227 | }, | |
0d9520c4 FE |
228 | 'prune-backups' => get_standard_option('prune-backups', { |
229 | description => "Use these retention options instead of those from the storage configuration.", | |
230 | optional => 1, | |
231 | }), | |
e2a0a93b CE |
232 | remove => { |
233 | type => 'boolean', | |
1c527dfe FE |
234 | description => "Remove old backup files if there are more than " . |
235 | "'maxfiles' backup files or prune according to 'prune-backups'.", | |
e2a0a93b CE |
236 | optional => 1, |
237 | default => 1, | |
238 | }, | |
239 | pool => { | |
240 | type => 'string', | |
241 | description => 'Backup all known guest systems included in the specified pool.', | |
242 | optional => 1, | |
243 | } | |
244 | }; | |
245 | ||
246 | sub get_confdesc { | |
247 | return $confdesc; | |
248 | } | |
249 | ||
250 | # add JSON properties for create and set function | |
251 | sub json_config_properties { | |
252 | my $prop = shift; | |
253 | ||
254 | foreach my $opt (keys %$confdesc) { | |
255 | $prop->{$opt} = $confdesc->{$opt}; | |
256 | } | |
257 | ||
258 | return $prop; | |
259 | } | |
260 | ||
261 | my $vzdump_properties = { | |
262 | additionalProperties => 0, | |
263 | properties => json_config_properties({}), | |
264 | }; | |
265 | ||
266 | sub parse_vzdump_cron_config { | |
267 | my ($filename, $raw) = @_; | |
268 | ||
269 | my $jobs = []; # correct jobs | |
270 | ||
271 | my $ejobs = []; # mailfomerd lines | |
272 | ||
273 | my $jid = 1; # we start at 1 | |
274 | ||
275 | my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : ''); | |
276 | ||
277 | while ($raw && $raw =~ s/^(.*?)(\n|$)//) { | |
278 | my $line = $1; | |
279 | ||
280 | next if $line =~ m/^\#/; | |
281 | next if $line =~ m/^\s*$/; | |
282 | next if $line =~ m/^PATH\s*=/; # we always overwrite path | |
283 | ||
284 | if ($line =~ m|^(\d+)\s+(\d+)\s+\*\s+\*\s+(\S+)\s+root\s+(/\S+/)?(#)?vzdump(\s+(.*))?$|) { | |
285 | eval { | |
286 | my $minute = int($1); | |
287 | my $hour = int($2); | |
288 | my $dow = $3; | |
289 | my $param = $7; | |
290 | my $enabled = $5; | |
291 | ||
292 | my $dowhash = parse_dow($dow, 1); | |
293 | die "unable to parse day of week '$dow' in '$filename'\n" if !$dowhash; | |
294 | ||
295 | my $args = PVE::Tools::split_args($param); | |
296 | my $opts = PVE::JSONSchema::get_options($vzdump_properties, $args, 'vmid'); | |
297 | ||
298 | $opts->{enabled} = !defined($enabled); | |
299 | $opts->{id} = "$digest:$jid"; | |
300 | $jid++; | |
301 | $opts->{starttime} = sprintf "%02d:%02d", $hour, $minute; | |
302 | $opts->{dow} = &$dowhash_to_dow($dowhash); | |
303 | ||
533d6e50 FE |
304 | $fixup_prune_backups_option->($opts); # parse the property string |
305 | ||
e2a0a93b CE |
306 | push @$jobs, $opts; |
307 | }; | |
308 | my $err = $@; | |
309 | if ($err) { | |
310 | syslog ('err', "parse error in '$filename': $err"); | |
311 | push @$ejobs, { line => $line }; | |
312 | } | |
313 | } elsif ($line =~ m|^\S+\s+(\S+)\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S.*)$|) { | |
314 | syslog ('err', "warning: malformed line in '$filename'"); | |
315 | push @$ejobs, { line => $line }; | |
316 | } else { | |
317 | syslog ('err', "ignoring malformed line in '$filename'"); | |
318 | } | |
319 | } | |
320 | ||
321 | my $res = {}; | |
322 | $res->{digest} = $digest; | |
323 | $res->{jobs} = $jobs; | |
324 | $res->{ejobs} = $ejobs; | |
325 | ||
326 | return $res; | |
327 | } | |
328 | ||
329 | sub write_vzdump_cron_config { | |
330 | my ($filename, $cfg) = @_; | |
331 | ||
332 | my $out = "# cluster wide vzdump cron schedule\n"; | |
333 | $out .= "# Automatically generated file - do not edit\n\n"; | |
334 | $out .= "PATH=\"/usr/sbin:/usr/bin:/sbin:/bin\"\n\n"; | |
335 | ||
336 | my $jobs = $cfg->{jobs} || []; | |
337 | foreach my $job (@$jobs) { | |
338 | my $enabled = ($job->{enabled}) ? '' : '#'; | |
339 | my $dh = parse_dow($job->{dow}); | |
340 | my $dow; | |
341 | if ($dh->{mon} && $dh->{tue} && $dh->{wed} && $dh->{thu} && | |
342 | $dh->{fri} && $dh->{sat} && $dh->{sun}) { | |
343 | $dow = '*'; | |
344 | } else { | |
345 | $dow = &$dowhash_to_dow($dh, 1); | |
346 | $dow = '*' if !$dow; | |
347 | } | |
348 | ||
349 | my ($hour, $minute); | |
350 | ||
351 | die "no job start time specified\n" if !$job->{starttime}; | |
352 | if ($job->{starttime} =~ m/^(\d{1,2}):(\d{1,2})$/) { | |
353 | ($hour, $minute) = (int($1), int($2)); | |
354 | die "hour '$hour' out of range\n" if $hour < 0 || $hour > 23; | |
355 | die "minute '$minute' out of range\n" if $minute < 0 || $minute > 59; | |
356 | } else { | |
357 | die "unable to parse job start time\n"; | |
358 | } | |
359 | ||
360 | $job->{quiet} = 1; # we do not want messages from cron | |
361 | ||
362 | my $cmd = command_line($job); | |
363 | ||
364 | $out .= sprintf "$minute $hour * * %-11s root $enabled$cmd\n", $dow; | |
365 | } | |
366 | ||
367 | my $ejobs = $cfg->{ejobs} || []; | |
368 | foreach my $job (@$ejobs) { | |
369 | $out .= "$job->{line}\n" if $job->{line}; | |
370 | } | |
371 | ||
372 | return $out; | |
373 | } | |
374 | ||
375 | sub command_line { | |
376 | my ($param) = @_; | |
377 | ||
378 | my $cmd = "vzdump"; | |
379 | ||
380 | if ($param->{vmid}) { | |
381 | $cmd .= " " . join(' ', PVE::Tools::split_list($param->{vmid})); | |
382 | } | |
383 | ||
384 | foreach my $p (keys %$param) { | |
385 | next if $p eq 'id' || $p eq 'vmid' || $p eq 'starttime' || | |
386 | $p eq 'dow' || $p eq 'stdout' || $p eq 'enabled'; | |
387 | my $v = $param->{$p}; | |
388 | my $pd = $confdesc->{$p} || die "no such vzdump option '$p'\n"; | |
389 | if ($p eq 'exclude-path') { | |
390 | foreach my $path (split(/\0/, $v || '')) { | |
391 | $cmd .= " --$p " . PVE::Tools::shellquote($path); | |
392 | } | |
393 | } else { | |
7a9b527f | 394 | $v = join(",", PVE::Tools::split_list($v)) if $p eq 'mailto'; |
9e542a4f FE |
395 | $v = PVE::JSONSchema::print_property_string($v, 'prune-backups') |
396 | if $p eq 'prune-backups'; | |
397 | ||
e2a0a93b CE |
398 | $cmd .= " --$p " . PVE::Tools::shellquote($v) if defined($v) && $v ne ''; |
399 | } | |
400 | } | |
401 | ||
402 | return $cmd; | |
403 | } | |
404 | ||
405 | # bash completion helpers | |
406 | sub complete_backup_storage { | |
407 | ||
408 | my $cfg = PVE::Storage::config(); | |
409 | my $ids = $cfg->{ids}; | |
410 | ||
411 | my $nodename = PVE::INotify::nodename(); | |
412 | ||
413 | my $res = []; | |
414 | foreach my $sid (keys %$ids) { | |
415 | my $scfg = $ids->{$sid}; | |
416 | next if !PVE::Storage::storage_check_enabled($cfg, $sid, $nodename, 1); | |
417 | next if !$scfg->{content}->{backup}; | |
418 | push @$res, $sid; | |
419 | } | |
420 | ||
421 | return $res; | |
422 | } | |
423 | ||
424 | 1; |