]> git.proxmox.com Git - pve-guest-common.git/blame - PVE/VZDump/Common.pm
Fix: #2124 add zstd support
[pve-guest-common.git] / PVE / VZDump / Common.pm
CommitLineData
e2a0a93b
CE
1package PVE::VZDump::Common;
2
3use strict;
4use warnings;
5use Digest::SHA;
6
7use PVE::Tools;
8use PVE::SafeSyslog qw(syslog);
9use PVE::Storage;
10use PVE::Cluster qw(cfs_register_file);
11use PVE::JSONSchema qw(get_standard_option);
12
13cfs_register_file('vzdump.cron',
14 \&parse_vzdump_cron_config,
15 \&write_vzdump_cron_config);
16
17my $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
33sub 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
63my $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
226sub get_confdesc {
227 return $confdesc;
228}
229
230# add JSON properties for create and set function
231sub 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
241my $vzdump_properties = {
242 additionalProperties => 0,
243 properties => json_config_properties({}),
244};
245
246sub 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
307sub 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
353sub 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
380sub 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
3981;