#!/usr/bin/perl
#
# Check database for inconsistent entries and remove them

package HaCi::HaCi;

use warnings;
use strict;

local $|	= 1;

use FindBin;
use lib "$FindBin::Bin/../modules";
chdir "$FindBin::Bin/..";
my $workDir	= qx/pwd/;
chomp($workDir);
$ENV{workdir} = $workDir;

use Getopt::Std;
use Data::Dumper qw/Dumper/;
use HaCi::Conf qw/getConfigValue/;
use HaCi::Utils qw/getConfig getPlugins getTable initTables finalizeTables initCache/;

$Getopt::Std::STANDARD_HELP_VERSION = 1;
$main::VERSION                      = 0.1;
our $opts														= {};
getopts('cd', $opts);

my $mysqlDumpBin				= 'mysqldump';
my $mysqlDumpArgs				= q{-u'<USER>' -p'<PASS>' -h'<HOST>' '<DBNAME>'};
my $postgresqlDumpBin		= 'pg_dump';
my $postgresqlDumpArgs	= q{-U'<USER>' -h'<HOST>' '<DBNAME>'};

my $debug							= (exists $opts->{d}) ? $opts->{d} : 0;
my @busy							= ('|', '/', '-', '\\', '-');
my ($bc, $ec, $sc)		= (0, 0, 0);
my ($userTable, $groupTable, $rootTable, $rootACTable, $networkTable, $networkV6Table, $networkACTable, $networkPluginTable, 
	$templateTable, $templateEntryTable, $templateValueTable, $pluginTable, $pluginConfTable, $pluginValueTable, $settingTable)	= ();

our $conf; *conf		= \$HaCi::Conf::conf;

&HaCi::Conf::init($workDir);
&getConfig();

&init();
&main();

exit 0;

#--------------------------------------------
END {
	&finalizeTables();
}

sub init {
	&initTables();
	$userTable  = $conf->{var}->{TABLES}->{user};
	unless (defined $userTable) {
		die "Cannot get users. DB Error (user)\n";
	}
	$groupTable  = $conf->{var}->{TABLES}->{group};
	unless (defined $groupTable) {
		die "Cannot get groups. DB Error (group)\n";
	}
	$rootTable  = $conf->{var}->{TABLES}->{root};
	unless (defined $rootTable) {
		die "Cannot get roots. DB Error (root)\n";
	}
	$rootACTable  = $conf->{var}->{TABLES}->{rootAC};
	unless (defined $rootACTable) {
		die "Cannot get rootACs. DB Error (rootAC)\n";
	}
	$networkTable  = $conf->{var}->{TABLES}->{network};
	unless (defined $networkTable) {
		die "Cannot get Networks. DB Error (network)\n";
	}
	$networkV6Table  = $conf->{var}->{TABLES}->{networkV6};
	unless (defined $networkV6Table) {
		die "Cannot get Networks. DB Error (networkV6)\n";
	}
	$networkACTable  = $conf->{var}->{TABLES}->{networkAC};
	unless (defined $networkACTable) {
		die "Cannot get networkACs. DB Error (networkAC)\n";
	}
	$networkPluginTable  = $conf->{var}->{TABLES}->{networkPlugin};
	unless (defined $networkPluginTable) {
		die "Cannot get networkPlugins. DB Error (networkPlugin)\n";
	}
	$templateTable  = $conf->{var}->{TABLES}->{template};
	unless (defined $templateTable) {
		die "Cannot get templates. DB Error (template)\n";
	}
	$templateEntryTable  = $conf->{var}->{TABLES}->{templateEntry};
	unless (defined $templateEntryTable) {
		die "Cannot get templateEntrys. DB Error (templateEntry)\n";
	}
	$templateValueTable  = $conf->{var}->{TABLES}->{templateValue};
	unless (defined $templateValueTable) {
		die "Cannot get templateValues. DB Error (templateValue)\n";
	}
	$pluginTable  = $conf->{var}->{TABLES}->{plugin};
	unless (defined $pluginTable) {
		die "Cannot get plugins. DB Error (plugin)\n";
	}
	$pluginConfTable  = $conf->{var}->{TABLES}->{pluginConf};
	unless (defined $pluginConfTable) {
		die "Cannot get pluginConfs. DB Error (pluginConf)\n";
	}
	$pluginValueTable  = $conf->{var}->{TABLES}->{pluginValue};
	unless (defined $pluginValueTable) {
		die "Cannot get pluginValues. DB Error (pluginValue)\n";
	}
	$settingTable  = $conf->{var}->{TABLES}->{setting};
	unless (defined $settingTable) {
		die "Cannot get settings. DB Error (setting)\n";
	}
}

sub main {

	if ($opts->{c}) {
		print "\nPlease make sure to make a database backup before cleaning it up.\n";
		my $dbType		= &getConfigValue('db', 'dbtype');
		my $dumpBin		= '';
		my $dumpArgs	= '';
		if ($dbType eq 'mysql') {
			$dumpBin	= $mysqlDumpBin;
			$dumpArgs	= $mysqlDumpArgs;
		}
		elsif ($dbType eq 'postgresql') {
			$dumpBin	= $postgresqlDumpBin;
			$dumpArgs	= $postgresqlDumpArgs;
		}

		if ($dumpBin && system("which $dumpBin >/dev/null") == 0) {
			my $md			= qx/which $dumpBin/;
			chomp($md);
			my $dbConf	= $conf->{user}->{db};
			my $pwd			= qx/pwd/;
			chomp($pwd);
			my $dbnameO	= "$pwd/$dbConf->{dbname}.sql";
			my $dbname	= $dbnameO;
			my $cnter		= 1;
			while ( -f $dbname ) {
				$dbname	= $dbnameO . '.' . $cnter++;
			}
			#my $backupT	= $md . " -u'$dbConf->{dbuser}' -p'XXX' -h'$dbConf->{dbhost}' '$dbConf->{dbname}' > $dbname";
			my $backupT	= $md . ' ' . $dumpArgs . "> $dbname";
			$backupT		=~ s/<USER>/$dbConf->{dbuser}/g;
			$backupT		=~ s/<PASS>/XXX/g;
			$backupT		=~ s/<HOST>/$dbConf->{dbhost}/g;
			$backupT		=~ s/<DBNAME>/$dbConf->{dbname}/g;

			print "Found '$dumpBin':\n\t$backupT\nShould I run this dump? [y|N]: ";
			my $bb	= <STDIN>;
			chomp($bb);
			if (lc($bb) eq 'y') {
				#my $backupStr	= $md . " -u'$dbConf->{dbuser}' -p'$dbConf->{dbpass}' -h'$dbConf->{dbhost}' '$dbConf->{dbname}' > $dbname";
				my $backupStr	= $md . ' ' . $dumpArgs . "> $dbname";
				$backupStr		=~ s/<USER>/$dbConf->{dbuser}/g;
				$backupStr		=~ s/<PASS>/$dbConf->{dbpass}/g;
				$backupStr		=~ s/<HOST>/$dbConf->{dbhost}/g;
				$backupStr		=~ s/<DBNAME>/$dbConf->{dbname}/g;
				print qx($backupStr);
				if ($?) {
					print "... Dump was NOT successful!!!\n"; 
				} else {
					print "... Dump was successful\n"; 
				}
			}
		}

		print "Continue cleaning up database? [y|N]: ";
		my $con	= <STDIN>;
		print "\n";
		chomp($con);
		exit 0 unless lc($con) eq 'y';
	}

	&checkRootStuff();
	&checkBrokenNets();
	&checkNetIDStuff();
	&checkpluginIDStuff();
	&checkTmplIDStuff();
	print "\n$bc Items checked!\n";
	print '' . ($sc + $ec) . " bad entries found. Deleted [" . $sc . " successful | " . $ec . " unsuccessful]\n" if $opts->{c};

	if ($opts->{c} && $sc > 0) {
		my $error	= 0;
		my ($aclCacheHandle, $netCacheHandle)  = &initCache();
		if (defined $netCacheHandle) {
			warn "Cannot set Cache (netCache-DB!)\n" unless $netCacheHandle->set('DB', {});
			warn "Cannot set Cache (netCache-FILL!)\n" unless $netCacheHandle->set('FILL', {});
			warn "Cannot set Cache (netCache-NET!)\n" unless $netCacheHandle->set('NET', {});
		} else {
			$error	= 1;
		}

		if (defined $aclCacheHandle) {
			warn "Cannot set Cache (aclCache!)\n" unless $aclCacheHandle->set('HASH', {});
		} else {
			$error	= 1;
		}
		warn "Cannot flush the cache. Please press the 'Flush Cache' button in the web gui.\n\n" if $error;
	} else {
		print "\nCall this script with '-c' in order to cleanup these issues: $0 -c\n\n";
	}
}

sub checkRootStuff {

	my $oEC	= $ec;
	my $oSC	= $sc;

	print "\nSearch for entries with bad rootID...\n";
	# Get Roots
	my @rootst	= $rootTable->search(['ID'], 0, 0, 0, 1);
	if ($#rootst == -1) {
		print "\r No roots available\n";
		return;
	}

	my $roots		= '';
	foreach (@rootst) {
		$roots		.= ',' if $roots;
		$roots	.= $_->{ID}
	}

	# Get stale V4
	my $nriCol			= $networkTable->meth2Col('rootID');
	my @networksNR	= $networkTable->search(['ID', 'network', 'description', 'rootID', 'ipv6ID'], {}, 0, "WHERE $nriCol NOT IN ($roots)");
	foreach (@networksNR) {
		my $ip	= $_;
		print "\r" . $busy[$bc++%5];
		print "\r $ip->{network}:$ip->{rootID} ($ip->{ipv6ID}) => $ip->{description}: Stale V4 network entry (root doesn't exists)\n" if $debug;
		&delEntry($networkTable, $ip->{ID});
	}
	print "\r " . ($#networksNR + 1) . " stale V4 network entrys found without matching root\n";

	# Get stale V6
	my $nriV6Col			= $networkV6Table->meth2Col('rootID');
	my @networkV6sNR	= $networkV6Table->search(['ID', 'rootID'], {}, 0, "WHERE $nriV6Col NOT IN ($roots)");
	foreach (@networkV6sNR) {
		my $ip	= $_;
		print "\r" . $busy[$bc++%5];
		print "\r $ip->{ID} : $ip->{rootID}: Stale V6 network entry (root doesn't exists)\n" if $debug;
		&delEntry($networkV6Table, {ID => $ip->{ID}, rootID => $ip->{rootID}});
	}
	print "\r " . ($#networkV6sNR + 1) . " stale V6 network entrys found without matching root\n";

	# Get stale rootAC
	my $rriCol		= $rootACTable->meth2Col('rootID');
	my @rootACsNR	= $rootACTable->search(['ID', 'rootID'], {}, 0, "WHERE $rriCol NOT IN ($roots)");
	foreach (@rootACsNR) {
		my $ac	= $_;
		print "\r" . $busy[$bc++%5];
		print "\r $ac->{ID} : $ac->{rootID}: Stale root ACL entry (root doesn't exists)\n" if $debug;
		&delEntry($rootACTable, $ac->{ID});
	}
	print "\r " . ($#rootACsNR + 1) . " stale root ACL entrys found without matching root\n";
	print "\n" if $debug;
	print '' . (($sc - $oSC) + ($ec - $oEC)) . " bad entries found. Deleted [" . ($sc - $oSC) . " successful | " . ($ec - $oEC) . " unsuccessful]\n" if $opts->{c};
}

sub checkBrokenNets {
	my $oEC	= $ec;
	my $oSC	= $sc;

	print "\nSearch for corrupt V6 networks...\n";

	# get corrupt V6 networks
	my $sNError				= 0;
	my $brokenNError	= 0;
	my @networks			= $networkTable->search(['ID', 'network', 'description', 'rootID', 'ipv6ID'], {network => 0});
	foreach (@networks) {
		my $ip		= $_;
		my $v6ID	= $_->{ipv6ID};
		print "\r" . $busy[$bc++%5];
		unless ($v6ID) {
			print "\r $ip->{network}:$ip->{rootID} ($ip->{ipv6ID}) => $ip->{description}: NO network AND no v6ID!\n" if $debug;
			$brokenNError++;
			&delEntry($networkTable, $ip->{ID});
			next;
		}

		my $ip6	= ($networkV6Table->search(['ID'], {ID => $ip->{ipv6ID}, rootID => $ip->{rootID}}))[0];
		if (!defined $ip6) {
			print "\r $ip->{network}:$ip->{rootID} ($ip->{ipv6ID}) => $ip->{description}: Stale network entry (no V6 part)\n" if $debug;
			&delEntry($networkTable, $ip->{ID});
			$sNError++;
		}
	}
	print "\r $sNError stale network entrys found without matching V6 Part\n";
	print " $brokenNError broken network entrys found\n";

	my $sN6Error		= 0;
	my @v6Networks	= $networkV6Table->search(['ID', 'rootID']);
	foreach (@v6Networks) {
		my $ip		= $_;
		my $v6ID	= $_->{ID};
		print "\r" . $busy[$bc++%5];

		my $ip4	= ($networkTable->search(['ID'], {ipv6ID => $v6ID, rootID => $ip->{rootID}}))[0];
		if (!defined $ip4) {
			print "\r $v6ID : $ip->{rootID}: Stale V6 network entry (no network part)\n" if $debug;
			&delEntry($networkV6Table, {ID => $ip->{ID}, rootID => $ip->{rootID}});
			$sN6Error++;
		}
	}
	print "\r $sN6Error stale V6 network entrys found without matching network Part\n";
	print "\n" if $debug;
	print '' . (($sc - $oSC) + ($ec - $oEC)) . " bad entries found. Deleted [" . ($sc - $oSC) . " successful | " . ($ec - $oEC) . " unsuccessful]\n" if $opts->{c};
}

sub checkNetIDStuff {
	my $oEC	= $ec;
	my $oSC	= $sc;

	print "\nSearch for entries with bad network ID...\n";

	# Get Networks
	my @netst	= $networkTable->search(['ID'], 0, 0, 0, 1);
	if ($#netst == -1) {
		print "\r No networks available\n";
		return;
	}

	my $nets		= {};
	foreach (@netst) {
		$nets->{$_->{ID}}	= 1;
	}

	# Get stale networkAC
	my @networkACs	= $networkACTable->search(['ID', 'netID', 'ACL']);
	my $cnter				= 0;
	foreach (@networkACs) {
		my $ac	= $_;
		next if !$ac->{netID} && $ac->{ACL} == 4;
		print "\r" . $busy[$bc++%5];
		if (!exists $nets->{$ac->{netID}}) {
			$cnter++;
			&delEntry($networkACTable, $ac->{ID});
			print "\r ACL $ac->{ID} : $ac->{netID}: Stale network ACL entry (network doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale network ACL entrys found without matching network\n";
	print "\n" if $debug;

	# Get stale network plugins
	my @networkPlugins	= $networkPluginTable->search(['ID', 'netID']);
	$cnter							= 0;
	foreach (@networkPlugins) {
		my $plugin	= $_;
		print "\r" . $busy[$bc++%5];
		if ($plugin->{netID} != -1 && !exists $nets->{$plugin->{netID}}) {
			$cnter++;
			&delEntry($networkPluginTable, $plugin->{ID});
			print "\r ID $plugin->{ID} - netID $plugin->{netID}: Stale network plugin entry (network doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale network Plugin entrys found without matching network\n";
	print "\n" if $debug;

	# Get stale plugin confs
	my @pluginConfs	= $pluginConfTable->search(['ID', 'netID']);
	$cnter					= 0;
	foreach (@pluginConfs) {
		my $plugin	= $_;
		print "\r" . $busy[$bc++%5];
		if ($plugin->{netID} != -1 && !exists $nets->{$plugin->{netID}}) {
			$cnter++;
			&delEntry($pluginConfTable, $plugin->{ID});
			print "\r ID $plugin->{ID} - netID $plugin->{netID}: Stale plugin config entry (network doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale plugin config entrys found without matching network\n";
	print "\n" if $debug;

	# Get stale plugin values
	my @pluginValues	= $pluginValueTable->search(['ID', 'netID']);
	$cnter					= 0;
	foreach (@pluginValues) {
		my $plugin	= $_;
		print "\r" . $busy[$bc++%5];
		if ($plugin->{netID} != -1 && !exists $nets->{$plugin->{netID}}) {
			$cnter++;
			&delEntry($pluginValueTable, $plugin->{ID});
			print "\r ID $plugin->{ID} - netID $plugin->{netID}: Stale plugin value entry (network doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale plugin value entrys found without matching network\n";
	print "\n" if $debug;

	# Get stale template values
	my @templateValues	= $templateValueTable->search(['ID', 'netID']);
	$cnter					= 0;
	foreach (@templateValues) {
		my $template	= $_;
		print "\r" . $busy[$bc++%5];
		if ($template->{netID} != -1 && !exists $nets->{$template->{netID}}) {
			$cnter++;
			&delEntry($templateValueTable, $template->{ID});
			print "\r ID $template->{ID} - netID $template->{netID}: Stale template value entry (network doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale template value entrys found without matching network\n";
	print "\n" if $debug;
	print '' . (($sc - $oSC) + ($ec - $oEC)) . " bad entries found. Deleted [" . ($sc - $oSC) . " successful | " . ($ec - $oEC) . " unsuccessful]\n" if $opts->{c};
}

sub checkpluginIDStuff {
	my $oEC	= $ec;
	my $oSC	= $sc;

	print "\nSearch for entries with bad plugin ID...\n";

	# Get Plugins
	my @pluginst	= $pluginTable->search(['ID'], 0, 0, 0, 1);
	if ($#pluginst == -1) {
		print "\r No plugins available\n";
		return;
	}

	my $plugins		= {};
	foreach (@pluginst) {
		$plugins->{$_->{ID}}	= 1;
	}

	# Get stale network plugins
	my @networkPlugins	= $networkPluginTable->search(['ID', 'pluginID']);
	my $cnter						= 0;
	foreach (@networkPlugins) {
		my $plugin	= $_;
		print "\r" . $busy[$bc++%5];
		if (!exists $plugins->{$plugin->{pluginID}}) {
			$cnter++;
			&delEntry($networkPluginTable, $plugin->{ID});
			print "\r ID $plugin->{ID} - netID $plugin->{pluginID}: Stale network plugin entry (plugin doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale network Plugin entrys found without matching plugin\n";
	print "\n" if $debug;

	# Get stale plugin confs
	my @pluginConfs	= $pluginConfTable->search(['ID', 'pluginID']);
	$cnter					= 0;
	foreach (@pluginConfs) {
		my $plugin	= $_;
		print "\r" . $busy[$bc++%5];
		if (!exists $plugins->{$plugin->{pluginID}}) {
			$cnter++;
			&delEntry($pluginConfTable, $plugin->{ID});
			print "\r ID $plugin->{ID} - pluginID $plugin->{pluginID}: Stale plugin config entry (plugin doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale plugin config entrys found without matching plugin\n";
	print "\n" if $debug;

	# Get stale plugin values
	my @pluginValues	= $pluginValueTable->search(['ID', 'pluginID']);
	$cnter						= 0;
	foreach (@pluginValues) {
		my $plugin	= $_;
		print "\r" . $busy[$bc++%5];
		if (!exists $plugins->{$plugin->{pluginID}}) {
			$cnter++;
			&delEntry($pluginValueTable, $plugin->{ID});
			print "\r ID $plugin->{ID} - pluginID $plugin->{pluginID}: Stale plugin value entry (plugin doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale plugin value entrys found without matching plugin\n";
	print "\n" if $debug;
	print '' . (($sc - $oSC) + ($ec - $oEC)) . " bad entries found. Deleted [" . ($sc - $oSC) . " successful | " . ($ec - $oEC) . " unsuccessful]\n" if $opts->{c};
}

sub checkTmplIDStuff {
	my $oEC	= $ec;
	my $oSC	= $sc;

	print "\nSearch for entries with bad template ID...\n";

	# Get tmpls
	my @tmplst	= $templateTable->search(['ID'], 0, 0, 0, 1);
	if ($#tmplst == -1) {
		print "\r No templates available\n";
		return;
	}

	my $tmpls		= {
		0	=> 1
	};
	foreach (@tmplst) {
		$tmpls->{$_->{ID}}	= 1;
	}

	# Get stale networks
	my @networks	= $networkTable->search(['ID', 'tmplID']);
	my $cnter						= 0;
	foreach (@networks) {
		my $net	= $_;
		print "\r" . $busy[$bc++%5];
		if (!exists $tmpls->{$net->{tmplID}}) {
			$cnter++;
			if ($opts->{c}) {
				$networkTable->tmplID(0);
				unless ($networkTable->update({ID => $net->{ID}})) {
					warn $networkTable->errorStrs();
					$ec++;
				} else {
					$sc++;
				}
			}
			print "\r ID $net->{ID} - tmplID $net->{tmplID}: Stale network entry (template doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale network entrys found without matching template\n";
	print "\n" if $debug;

	# Get stale template Entrys
	my @tmplEntrys	= $templateEntryTable->search(['ID', 'tmplID']);
	$cnter					= 0;
	foreach (@tmplEntrys) {
		my $tmplEntry	= $_;
		print "\r" . $busy[$bc++%5];
		if (!exists $tmpls->{$tmplEntry->{tmplID}}) {
			$cnter++;
			&delEntry($templateEntryTable, $tmplEntry->{ID});
			print "\r ID $tmplEntry->{ID} - tmplID $tmplEntry->{tmplID}: Stale templateEntry entry (template doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale templateEntry entrys found without matching template\n";
	print "\n" if $debug;

	# Get stale template Values
	my @tmplValues	= $templateValueTable->search(['ID', 'tmplID']);
	$cnter					= 0;
	foreach (@tmplValues) {
		my $tmplValue	= $_;
		print "\r" . $busy[$bc++%5];
		if (!exists $tmpls->{$tmplValue->{tmplID}}) {
			$cnter++;
			&delEntry($templateValueTable, $tmplValue->{ID});
			print "\r ID $tmplValue->{ID} - tmplID $tmplValue->{tmplID}: Stale templateValue entry (template doesn't exists)\n" if $debug;
		}
	}
	print "\r $cnter stale templateValue entrys found without matching template\n";
	print "\n" if $debug;
	print '' . (($sc - $oSC) + ($ec - $oEC)) . " bad entries found. Deleted/Modified [" . ($sc - $oSC) . " successful | " . ($ec - $oEC) . " unsuccessful]\n" if $opts->{c};
}

sub main::HELP_MESSAGE {
	print "
NAME
	cleanupDatabase.pl - clean up HaCi database

SYNOPSIS
	$0 [-c] [-d]

OPTIONS
	-c	Clean up issues. Without this option issues will only be reported.
	-d	Debug mode. Show every issue.

	Please make sure to make a database backup before cleaning it up. 

";
}

sub delEntry {
	my $table	= shift;
	my $ID		= shift;
	my $pk		= (ref($ID) eq 'HASH') ? $ID : {ID => $ID};

	if ($opts->{c}) {
		unless($table->delete($pk)) {
			warn $table->errorStrs();
			$ec++;
		} else {
			$sc++;
		}
	}
}

# vim:ts=2:sts=2:sw=2
