]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Backup.pm
dkim: add QID in warnings
[pmg-api.git] / src / PMG / Backup.pm
CommitLineData
9a8d51a4
DM
1package PMG::Backup;
2
3use strict;
4use warnings;
5use Data::Dumper;
6use File::Basename;
1e0861f9 7use File::Find;
9a8d51a4 8use File::Path;
f8c773d8 9use POSIX qw(strftime);
9a8d51a4 10
cc9a5f58 11use PVE::JSONSchema qw(get_standard_option);
9a8d51a4
DM
12use PVE::Tools;
13
14use PMG::pmgcfg;
15use PMG::AtomicFile;
6529020a 16use PMG::Utils qw(postgres_admin_cmd);
9a8d51a4 17
dd290005
SI
18my $sa_configs = [
19 "/etc/mail/spamassassin/custom.cf",
20 "/etc/mail/spamassassin/pmg-scores.cf",
21];
520f7717 22
cc9a5f58
SI
23sub get_restore_options {
24 return (
25 node => get_standard_option('pve-node'),
26 config => {
27 description => "Restore system configuration.",
28 type => 'boolean',
29 optional => 1,
30 default => 0,
31 },
32 database => {
33 description => "Restore the rule database. This is the default.",
34 type => 'boolean',
35 optional => 1,
36 default => 1,
37 },
38 statistic => {
39 description => "Restore statistic databases. Only considered when you restore the 'database'.",
40 type => 'boolean',
41 optional => 1,
42 default => 0,
43 });
44}
45
9a8d51a4
DM
46sub dump_table {
47 my ($dbh, $table, $ofh, $seq, $seqcol) = @_;
48
49 my $sth = $dbh->column_info(undef, undef, $table, undef);
50
51 my $attrs = $sth->fetchall_arrayref({});
52
53 my @col_arr;
54 foreach my $ref (@$attrs) {
55 push @col_arr, $ref->{COLUMN_NAME};
56 }
57
58 $sth->finish();
59
60 my $cols = join (', ', @col_arr);
61 $cols || die "unable to fetch column definitions: ERROR";
62
63 print $ofh "COPY $table ($cols) FROM stdin;\n";
64
65 my $cmd = "COPY $table ($cols) TO STDOUT";
66 $dbh->do($cmd);
67
68 my $data = '';
69 while ($dbh->pg_getcopydata($data) >= 0) {
70 print $ofh $data;
71 }
72
73 print $ofh "\\.\n\n";
74
75 if ($seq && $seqcol) {
76 print $ofh "SELECT setval('$seq', max($seqcol)) FROM $table;\n\n";
77 }
78}
79
80sub dumpdb {
81 my ($ofh) = @_;
82
83 print $ofh "SET client_encoding = 'SQL_ASCII';\n";
84 print $ofh "SET check_function_bodies = false;\n\n";
85
86 my $dbh = PMG::DBTools::open_ruledb();
87
88 print $ofh "BEGIN TRANSACTION;\n\n";
89
90 eval {
91 $dbh->begin_work;
92
93 # read a consistent snapshot
94 $dbh->do("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
95
81d6c9c1
SI
96 dump_table($dbh, 'objectgroup', $ofh, 'objectgroup_id_seq', 'id');
97 dump_table($dbh, 'object', $ofh, 'object_id_seq', 'id');
9a8d51a4 98 dump_table($dbh, 'attribut', $ofh);
45d3ef6f 99 dump_table($dbh, 'objectgroup_attributes', $ofh);
9a8d51a4 100 dump_table($dbh, 'rule', $ofh, 'rule_id_seq', 'id');
81d6c9c1 101 dump_table($dbh, 'rule_attributes', $ofh);
9a8d51a4
DM
102 dump_table($dbh, 'rulegroup', $ofh);
103 dump_table($dbh, 'userprefs', $ofh);
104
105 # we do not save the following tables: cgreylist, cmailstore, cmsreceivers, clusterinfo
106 };
107 my $err = $@;
108
109 $dbh->rollback(); # end read-only transaction
110
111 $dbh->disconnect();
112
113 die $err if $err;
114
115 print $ofh "COMMIT TRANSACTION;\n\n";
116}
117
118sub dumpstatdb {
119 my ($ofh) = @_;
120
121 print $ofh "SET client_encoding = 'SQL_ASCII';\n";
122 print $ofh "SET check_function_bodies = false;\n\n";
123
124 my $dbh = PMG::DBTools::open_ruledb();
125
126 eval {
127 $dbh->begin_work;
128
129 # read a consistent snapshot
130 $dbh->do("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
131
132 print $ofh "BEGIN TRANSACTION;\n\n";
133
134 dump_table($dbh, 'dailystat', $ofh);
135 dump_table($dbh, 'domainstat', $ofh);
136 dump_table($dbh, 'virusinfo', $ofh);
137 dump_table($dbh, 'localstat', $ofh);
138
139 # drop/create the index is a little bit faster (20%)
140
141 print $ofh "DROP INDEX cstatistic_time_index;\n\n";
142 print $ofh "ALTER TABLE cstatistic DROP CONSTRAINT cstatistic_id_key;\n\n";
143 print $ofh "ALTER TABLE cstatistic DROP CONSTRAINT cstatistic_pkey;\n\n";
144 dump_table($dbh, 'cstatistic', $ofh, 'cstatistic_id_seq', 'id');
145 print $ofh "ALTER TABLE ONLY cstatistic ADD CONSTRAINT cstatistic_pkey PRIMARY KEY (cid, rid);\n\n";
146 print $ofh "ALTER TABLE ONLY cstatistic ADD CONSTRAINT cstatistic_id_key UNIQUE (id);\n\n";
147 print $ofh "CREATE INDEX CStatistic_Time_Index ON CStatistic (Time);\n\n";
148
149 print $ofh "DROP INDEX CStatistic_ID_Index;\n\n";
150 dump_table($dbh, 'creceivers', $ofh);
151 print $ofh "CREATE INDEX CStatistic_ID_Index ON CReceivers (CStatistic_CID, CStatistic_RID);\n\n";
152
153 dump_table($dbh, 'statinfo', $ofh);
154
155 print $ofh "COMMIT TRANSACTION;\n\n";
156 };
157 my $err = $@;
158
159 $dbh->rollback(); # end read-only transaction
160
161 $dbh->disconnect();
162
163 die $err if $err;
164}
165
708cc7a9 166# this function assumes that directory $dirname exists and is empty
9a8d51a4 167sub pmg_backup {
29e2ff48
SI
168 my ($dirname, $include_statistics) = @_;
169
170 die "No backupdir provided!\n" if !defined($dirname);
9a8d51a4
DM
171
172 my $time = time;
9a8d51a4
DM
173 my $dbfn = "Proxmox_ruledb.sql";
174 my $statfn = "Proxmox_statdb.sql";
175 my $tarfn = "config_backup.tar";
176 my $sigfn = "proxmox_backup_v1.md5";
177 my $verfn = "version.txt";
178
179 eval {
180
9a8d51a4
DM
181 # dump the database first
182 my $fh = PMG::AtomicFile->open("$dirname/$dbfn", "w") ||
1359baef 183 die "can't open '$dirname/$dbfn' - $! :ERROR";
9a8d51a4
DM
184
185 dumpdb($fh);
186
187 $fh->close(1);
188
189 if ($include_statistics) {
190 # dump the statistic db
191 my $sfh = PMG::AtomicFile->open("$dirname/$statfn", "w") ||
1359baef 192 die "can't open '$dirname/$statfn' - $! :ERROR";
9a8d51a4
DM
193
194 dumpstatdb($sfh);
195
196 $sfh->close(1);
197 }
198
199 my $pkg = PMG::pmgcfg::package();
1c15ce0f 200 my $release = PMG::pmgcfg::release();
9a8d51a4
DM
201
202 my $vfh = PMG::AtomicFile->open ("$dirname/$verfn", "w") ||
1359baef 203 die "can't open '$dirname/$verfn' - $! :ERROR";
9a8d51a4
DM
204
205 $time = time;
206 my $now = localtime;
1c15ce0f 207 print $vfh "product: $pkg\nversion: $release\nbackuptime:$time:$now\n";
9a8d51a4
DM
208 $vfh->close(1);
209
bd1325ed 210 my $extra_cfgs = [];
9a8d51a4 211
dd290005 212 push @$extra_cfgs, @{$sa_configs};
9a8d51a4 213
9a8d51a4
DM
214 my $extradb = $include_statistics ? $statfn : '';
215
bd1325ed 216 my $extra = join(' ', @$extra_cfgs);
9a8d51a4
DM
217
218 system("/bin/tar cf $dirname/$tarfn -C / " .
bd1325ed 219 "/etc/pmg $extra>/dev/null 2>&1") == 0 ||
9a8d51a4
DM
220 die "unable to create system configuration backup: ERROR";
221
222 system("cd $dirname; md5sum $tarfn $dbfn $extradb $verfn> $sigfn") == 0 ||
223 die "unable to create backup signature: ERROR";
224
29e2ff48
SI
225 };
226 my $err = $@;
227
228 if ($err) {
229 die $err;
230 }
231}
232
233sub pmg_backup_pack {
234 my ($filename, $include_statistics) = @_;
235
236 my $time = time;
237 my $dirname = "/tmp/proxbackup_$$.$time";
238
239 eval {
240
241 my $targetdir = dirname($filename);
242 mkdir $targetdir; # try to create target dir
243 -d $targetdir ||
244 die "unable to access target directory '$targetdir'\n";
245
246 rmtree $dirname;
247 # create backup directory
248 mkdir $dirname;
249
250 pmg_backup($dirname, $include_statistics);
251
252 system("rm -f $filename; tar czf $filename --strip-components=1 -C $dirname .") == 0 ||
f44d0cae 253 die "unable to create backup archive: ERROR\n";
9a8d51a4
DM
254 };
255 my $err = $@;
256
257 rmtree $dirname;
258
259 if ($err) {
260 unlink $filename;
261 die $err;
262 }
263}
264
a6d276e9
DM
265sub pmg_restore {
266 my ($filename, $restore_database, $restore_config, $restore_statistics) = @_;
267
268 my $dbname = 'Proxmox_ruledb';
269
270 my $time = time;
271 my $dirname = "/tmp/proxrestore_$$.$time";
272 my $dbfn = "Proxmox_ruledb.sql";
273 my $statfn = "Proxmox_statdb.sql";
274 my $tarfn = "config_backup.tar";
275 my $sigfn = "proxmox_backup_v1.md5";
276
2cf02541
SI
277 my $untar = 1;
278
279 # directory indicates that the files were restored from a PBS remote
280 if ( -d $filename ) {
281 $dirname = $filename;
282 $untar = 0;
283 }
284
a6d276e9 285 eval {
a6d276e9 286
2cf02541 287 if ($untar) {
e9c4929f
DM
288 # remove any leftovers
289 rmtree $dirname;
2cf02541
SI
290 # create a temporary directory
291 mkdir $dirname;
292
293 system("cd $dirname; tar xzf $filename >/dev/null 2>&1") == 0 ||
294 die "unable to extract backup archive: ERROR";
295 }
a6d276e9
DM
296
297 system("cd $dirname; md5sum -c $sigfn") == 0 ||
298 die "proxmox backup signature check failed: ERROR";
299
300 if ($restore_config) {
301 # restore the tar file
302 mkdir "$dirname/config/";
303 system("tar xpf $dirname/$tarfn -C $dirname/config/") == 0 ||
304 die "unable to restore configuration tar archive: ERROR";
305
306 -d "$dirname/config/etc/pmg" ||
307 die "backup does not contain a valid system configuration directory (/etc/pmg)\n";
308 # unlink unneeded files
309 unlink "$dirname/config/etc/pmg/cluster.conf"; # never restore cluster config
310 rmtree "$dirname/config/etc/pmg/master";
311
1e0861f9 312 # remove current config, but keep directories for INotify
2579f964
TL
313 File::Find::find(
314 sub {
315 my $file = $File::Find::name;
316 return if -d $file;
3f7d9808 317 unlink($file) || $! == POSIX::ENOENT || die "removing $file failed: $!\n";
2579f964
TL
318 },
319 '/etc/pmg',
320 );
1e0861f9 321
a6d276e9
DM
322 # copy files
323 system("cp -a $dirname/config/etc/pmg/* /etc/pmg/") == 0 ||
324 die "unable to restore system configuration: ERROR";
325
dd290005
SI
326 for my $sa_cfg (@{$sa_configs}) {
327 if (-f "$dirname/config/${sa_cfg}") {
328 my $data = PVE::Tools::file_get_contents(
329 "$dirname/config/${sa_cfg}", 1024*1024);
330 PVE::Tools::file_set_contents($sa_cfg, $data);
331 }
520f7717
DM
332 }
333
a6d276e9
DM
334 my $cfg = PMG::Config->new();
335 my $ruledb = PMG::RuleDB->new();
336 my $rulecache = PMG::RuleCache->new($ruledb);
337 $cfg->rewrite_config($rulecache, 1);
338 }
339
340 if ($restore_database) {
341 # recreate the database
342
343 # stop all services accessing the database
344 PMG::Utils::service_wait_stopped(40, $PMG::Utils::db_service_list);
345
346 print "Destroy existing rule database\n";
347 PMG::DBTools::delete_ruledb($dbname);
348
349 print "Create new database\n";
350 my $dbh = PMG::DBTools::create_ruledb($dbname);
a6d276e9
DM
351
352 system("cat $dirname/$dbfn|psql $dbname >/dev/null 2>&1") == 0 ||
353 die "unable to restore rule database: ERROR";
354
355 if ($restore_statistics) {
356 if (-f "$dirname/$statfn") {
357 system("cat $dirname/$statfn|psql $dbname >/dev/null 2>&1") == 0 ||
358 die "unable to restore statistic database: ERROR";
359 }
360 }
361
362 print STDERR "run analyze to speed up database queries\n";
6529020a 363 postgres_admin_cmd('psql', { input => 'analyze;' }, $dbname);
a6d276e9
DM
364
365 print "Analyzing/Upgrading existing Databases...";
9882b397 366 my $ruledb = PMG::RuleDB->new($dbh);
a6d276e9
DM
367 PMG::DBTools::upgradedb($ruledb);
368 print "done\n";
369
370 # cleanup old spam/virus storage
371 PMG::MailQueue::create_spooldirs(0, 1);
372
373 my $cfg = PMG::Config->new();
374 my $rulecache = PMG::RuleCache->new($ruledb);
375 $cfg->rewrite_config($rulecache, 1);
376
377 # and restart services as soon as possible
378 foreach my $service (reverse @$PMG::Utils::db_service_list) {
379 eval { PVE::Tools::run_command(['systemctl', 'start', $service]); };
380 warn $@ if $@;
381 }
382 }
383 };
384 my $err = $@;
385
386 rmtree $dirname;
387
388 die $err if $err;
389}
390
f8c773d8
SI
391sub send_backup_notification {
392 my ($notify_on, $target, $log, $err) = @_;
393
394 return if !$notify_on;
395 return if $notify_on eq 'never';
396 return if $notify_on eq 'error' && !$err;
397
398 my $cfg = PMG::Config->new();
399 my $email = $cfg->get ('admin', 'email');
400 if (!$email) {
1359baef 401 warn "not sending notification: no admin email configured\n";
f8c773d8
SI
402 return;
403 }
404
405 my $nodename = PVE::INotify::nodename();
406 my $fqdn = PVE::Tools::get_fqdn($nodename);
407
408
409 my $vars = {
410 hostname => $nodename,
411 fqdn => $fqdn,
412 date => strftime("%F", localtime()),
413 target => $target,
414 log => $log,
415 err => $err,
416 };
417
418 my $tt = PMG::Config::get_template_toolkit();
419
420 my $mailfrom = "Proxmox Mail Gateway <postmaster>";
421 PMG::Utils::finalize_report($tt, 'backup-notification.tt', $vars, $mailfrom, $email);
422
423}
424
9a8d51a4 4251;