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