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