garbage.pl 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #!/usr/bin/perl
  2. #
  3. # Copyright (C) Roman Dmitriev, rnd@rajven.ru
  4. #
  5. use utf8;
  6. use open ":encoding(utf8)";
  7. use Encode;
  8. no warnings 'utf8';
  9. use English;
  10. use base;
  11. use FindBin '$Bin';
  12. use lib "/opt/Eye/scripts";
  13. use strict;
  14. use DBI;
  15. use Date::Parse;
  16. use eyelib::config;
  17. use eyelib::database;
  18. use eyelib::common;
  19. use eyelib::net_utils;
  20. use eyelib::main;
  21. use DateTime;
  22. use Fcntl qw(:flock);
  23. # Ensure only one instance of the script runs at a time
  24. open(SELF, "<", $0) or die "Cannot open $0 - $!";
  25. flock(SELF, LOCK_EX | LOCK_NB) or exit 1;
  26. # Number of days to retain debug-level log entries
  27. my $debug_history = 3;
  28. # List of database tables eligible for optimization
  29. #my @db_tables = (
  30. # 'connections',
  31. # 'device_l3_interfaces',
  32. # 'device_ports',
  33. # 'user_list',
  34. # 'user_auth',
  35. # 'unknown_mac',
  36. # 'user_stats',
  37. # 'user_stats_full',
  38. # 'dhcp_log',
  39. # 'worklog',
  40. # 'remote_syslog',
  41. # 'traffic_detail',
  42. #);
  43. # Optimization flag (disabled by default)
  44. #my $optimize = 0;
  45. # Enable table optimization if "optimize" is passed as the first argument
  46. #if ($ARGV[0] =~ /optimize/i) {
  47. # $optimize = 1;
  48. #}
  49. log_info($dbh, 'Garbage collection started.');
  50. # Helper: Check if a given IP (as integer) belongs to any DHCP pool
  51. sub is_dhcp_pool {
  52. my $pools = shift;
  53. my $ip_int = shift;
  54. foreach my $subnet (keys %{$pools}) {
  55. # Undescription for debugging:
  56. # print "net: $subnet ip: $ip_int pool: $pools->{$subnet}->{first_ip} .. $pools->{$subnet}->{last_ip}\n";
  57. if ($ip_int <= $pools->{$subnet}->{last_ip} && $ip_int >= $pools->{$subnet}->{first_ip}) {
  58. return $subnet;
  59. }
  60. }
  61. return 0;
  62. }
  63. # Get current date components
  64. my ($sec, $min, $hour, $day, $month, $year, $zone) = localtime(time());
  65. my $history_sql;
  66. my $history_rf;
  67. # Build Patricia tries for office networks (used for IP matching)
  68. my %nets;
  69. my %dhcp_conf;
  70. foreach my $net (@office_network_list) {
  71. my $scope_name = $net;
  72. $scope_name =~ s/\/\d+$//g; # Strip CIDR suffix (e.g., /24)
  73. $nets{$scope_name} = Net::Patricia->new;
  74. $nets{$scope_name}->add_string($net);
  75. }
  76. # Define the current month’s start and end (for monthly stats)
  77. my $now = DateTime->now(time_zone => 'local');
  78. $now->set(day => 1);
  79. my $month_start = $dbh->quote($now->ymd("-") . " 00:00:00");
  80. my $month_dur = DateTime::Duration->new(months => 1);
  81. my $next_month = $now + $month_dur;
  82. $next_month->set(day => 1);
  83. my $month_stop = $dbh->quote($next_month->ymd("-") . " 00:00:00");
  84. # Build DHCP network structures for lease validation
  85. my $dhcp_networks = Net::Patricia->new;
  86. my @subnets = get_records_sql($dbh, 'SELECT * FROM subnets WHERE office = 1 AND dhcp = 1 AND vpn = 0 ORDER BY ip_int_start');
  87. foreach my $subnet (@subnets) {
  88. $dhcp_networks->add_string($subnet->{subnet});
  89. my $subnet_name = $subnet->{subnet};
  90. $subnet_name =~ s/\/\d+$//g;
  91. $dhcp_conf{$subnet_name}->{first_ip} = $subnet->{dhcp_start};
  92. $dhcp_conf{$subnet_name}->{last_ip} = $subnet->{dhcp_stop};
  93. }
  94. # On the 1st of the month: perform "monthly amnesty" — unblock all traffic-blocked users
  95. if ($day == 1) {
  96. log_info($dbh, 'Monthly amnesty started');
  97. db_log_info($dbh, "Unblocking all users blocked due to traffic quota");
  98. do_sql($dbh, "UPDATE user_list SET blocked = 0");
  99. do_sql($dbh, "UPDATE user_auth SET blocked = 0, changed = 1 WHERE blocked = 1 AND deleted = 0");
  100. log_info($dbh, 'Monthly amnesty completed');
  101. } else {
  102. # Daily: unblock users whose monthly traffic is now below quota
  103. log_info($dbh, 'Daily traffic-based unblocking started');
  104. my $month_sql = "
  105. SELECT user_list.id, user_list.login, SUM(traf_all) AS traf_sum, user_list.month_quota AS uquota
  106. FROM (
  107. SELECT user_stats.auth_id, SUM(byte_in + byte_out) AS traf_all
  108. FROM user_stats
  109. WHERE user_stats.ts >= $month_start AND user_stats.ts < $month_stop
  110. GROUP BY user_stats.auth_id
  111. ) AS V, user_auth, user_list
  112. WHERE V.auth_id = user_auth.id
  113. AND user_auth.user_id = user_list.id
  114. AND user_list.blocked = 1
  115. GROUP BY login
  116. ";
  117. my @month_stats = get_records_sql($dbh, $month_sql);
  118. foreach my $row (@month_stats) {
  119. my $m_quota = $row->{uquota} * $KB * $KB; # Convert MB to bytes
  120. next if ($m_quota < $row->{traf_sum}); # Skip if still over quota
  121. unblock_user($dbh, $row->{id});
  122. }
  123. log_info($dbh, 'Daily traffic-based unblocking completed');
  124. }
  125. # Clean expired DHCP leases for dynamic users (hotspot and default OU only)
  126. log_info($dbh, 'Cleaning DHCP leases with overdue expiration for dynamic hosts');
  127. my $users_sql = "SELECT * FROM user_auth WHERE deleted = 0 AND (ou_id = " . $default_user_ou_id . " OR ou_id = " . $default_hotspot_ou_id . ")";
  128. my @users_auth = get_records_sql($dbh, $users_sql);
  129. foreach my $row (@users_auth) {
  130. # Skip if IP is not in any DHCP pool
  131. next if (!is_dhcp_pool(\%dhcp_conf, $row->{ip_int}));
  132. # Only process IPs that belong to a DHCP-managed subnet
  133. if ($dhcp_networks->match_string($row->{ip})) {
  134. my $last_dhcp_time = GetUnixTimeByStr($row->{dhcp_time});
  135. # Lease timeout = last DHCP time + (60 * lease time in minutes)
  136. my $clean_dhcp_time = $last_dhcp_time + 60 * $dhcp_networks->match_string($row->{ip});
  137. if (time() > $clean_dhcp_time) {
  138. db_log_verbose($dbh, "Cleaning overdue DHCP lease for IP: $row->{ip}, auth_id: $row->{id}, last DHCP: $row->{dhcp_time}, clean time: " . GetTimeStrByUnixTime($clean_dhcp_time) . ", now: " . GetNowTime());
  139. delete_user_auth($dbh, $row->{id});
  140. # Also delete parent user if no other active sessions remain
  141. my $u_count = get_count_records($dbh, 'user_auth', "deleted = 0 AND user_id = " . $row->{user_id});
  142. if (!$u_count) {
  143. delete_user($dbh, $row->{'user_id'});
  144. db_log_info($dbh, "Removed dynamic user id: $row->{'user_id'} due to DHCP lease timeout");
  145. }
  146. }
  147. }
  148. }
  149. $now = DateTime->now(time_zone => 'local');
  150. # Clean old DHCP log entries (if retention policy is set)
  151. if ($history_dhcp) {
  152. my $day_dur = DateTime::Duration->new(days => $history_dhcp);
  153. my $clean_date = $now - $day_dur;
  154. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  155. log_info($dbh, 'Clearing outdated DHCP log records');
  156. do_sql($dbh, "DELETE FROM dhcp_log WHERE ts < $clean_str");
  157. log_verbose($dbh, "Removed DHCP log entries older than $clean_str");
  158. }
  159. # Clean old connection records (based on $connections_history setting)
  160. if ($connections_history) {
  161. log_info($dbh, 'Clearing outdated connection records');
  162. my $day_dur = DateTime::Duration->new(days => $connections_history);
  163. my $clean_date = $now - $day_dur;
  164. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  165. $users_sql = "SELECT id FROM user_auth WHERE last_found < $clean_str AND last_found > 0";
  166. log_debug($dbh, $users_sql) if ($debug);
  167. @users_auth = get_records_sql($dbh, $users_sql);
  168. foreach my $row (@users_auth) {
  169. log_debug($dbh, "Clearing old connection for auth_id: " . $row->{id});
  170. do_sql($dbh, "DELETE FROM connections WHERE auth_id = '" . $row->{id} . "'");
  171. }
  172. }
  173. # Remove duplicate connection records (same auth_id + port_id)
  174. log_info($dbh, 'Clearing duplicate connection records');
  175. my $conn_sql = "SELECT id, port_id, auth_id FROM connections ORDER BY port_id";
  176. my @conn_ref = get_records_sql($dbh, $conn_sql);
  177. my $old_port_id = 0;
  178. my $old_auth_id = 0;
  179. foreach my $row (@conn_ref) {
  180. my $c_id = $row->{id};
  181. my $c_port_id = $row->{port_id} || 0;
  182. my $c_auth_id = $row->{auth_id} || 0;
  183. if ($old_port_id == 0 || $old_auth_id == 0) {
  184. $old_port_id = $c_port_id;
  185. $old_auth_id = $c_auth_id;
  186. next;
  187. }
  188. # If we're still on the same (port, auth) pair, this is a duplicate
  189. if ($old_port_id == $c_port_id && $old_auth_id == $c_auth_id) {
  190. do_sql($dbh, "DELETE FROM connections WHERE id = '$c_id'");
  191. log_info($dbh, "Removed duplicate connection id=$c_id: port=$c_port_id auth=$c_auth_id");
  192. } else {
  193. $old_port_id = $c_port_id;
  194. $old_auth_id = $c_auth_id;
  195. }
  196. }
  197. # Clean empty dynamic/hotspot user accounts (no active authentications)
  198. log_info($dbh, 'Clearing empty user accounts and associated devices for dynamic users and hotspot');
  199. my $u_sql = "SELECT * FROM user_list AS U WHERE (U.ou_id = " . $default_hotspot_ou_id . " OR U.ou_id = " . $default_user_ou_id . ") AND (SELECT COUNT(*) FROM user_auth WHERE user_auth.deleted = 0 AND user_auth.user_id = U.id) = 0";
  200. my @u_ref = get_records_sql($dbh, $u_sql);
  201. foreach my $row (@u_ref) {
  202. db_log_info($dbh, "Removing empty dynamic user with id: $row->{id}, login: $row->{login}");
  203. delete_user($dbh, $row->{id});
  204. }
  205. # Clean empty non-permanent user accounts (if enabled in config)
  206. if ($config_ref{clean_empty_user}) {
  207. log_info($dbh, 'Clearing empty non-permanent user accounts and associated devices');
  208. my $u_sql = "SELECT * FROM user_list AS U WHERE U.permanent = 0 AND (SELECT COUNT(*) FROM user_auth WHERE user_auth.deleted = 0 AND user_auth.user_id = U.id) = 0 AND (SELECT COUNT(*) FROM auth_rules WHERE auth_rules.user_id = U.id) = 0;";
  209. my @u_ref = get_records_sql($dbh, $u_sql);
  210. foreach my $row (@u_ref) {
  211. db_log_info($dbh, "Removing empty user with id: $row->{id}, login: $row->{login}");
  212. delete_user($dbh, $row->{id});
  213. }
  214. }
  215. # Remove orphaned auth rules (no corresponding user)
  216. do_sql($dbh, "DELETE FROM auth_rules WHERE user_id NOT IN (SELECT id FROM user_list)");
  217. # Clean unknown MAC entries that now belong to known users
  218. log_info($dbh, 'Removing unknown MAC records that are now associated with active users');
  219. $users_sql = "SELECT mac FROM user_auth WHERE deleted = 0";
  220. @users_auth = get_records_sql($dbh, $users_sql);
  221. foreach my $row (@users_auth) {
  222. next if (!$row->{mac});
  223. do_sql($dbh, "DELETE FROM unknown_mac WHERE mac = '" . mac_simplify($row->{mac}) . "'");
  224. }
  225. # Clean old detailed traffic records (based on global $history setting)
  226. if ($history) {
  227. my $day_dur = DateTime::Duration->new(days => $history);
  228. my $clean_date = $now - $day_dur;
  229. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  230. log_info($dbh, "Cleaning traffic detail records older than $clean_str");
  231. do_sql($dbh, "DELETE FROM traffic_detail WHERE ts < $clean_str");
  232. }
  233. # Clean verbose (non-info) worklog entries
  234. if ($history_log_day) {
  235. my $day_dur = DateTime::Duration->new(days => $history_log_day);
  236. my $clean_date = $now - $day_dur;
  237. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  238. log_info($dbh, "Cleaning VERBOSE worklog entries older than $clean_str");
  239. do_sql($dbh, "DELETE FROM worklog WHERE level > $L_INFO AND ts < $clean_str");
  240. }
  241. # Clean debug-level worklog entries older than $debug_history days (hardcoded to 3)
  242. if ($debug_history) {
  243. my $day_dur = DateTime::Duration->new(days => 3);
  244. my $clean_date = $now - $day_dur;
  245. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  246. log_info($dbh, "Cleaning debug worklog entries older than $clean_str");
  247. do_sql($dbh, "DELETE FROM worklog WHERE level >= $L_DEBUG AND ts < $clean_str");
  248. }
  249. # Clean old remote syslog entries
  250. if ($history_syslog_day) {
  251. my $day_dur = DateTime::Duration->new(days => $history_syslog_day);
  252. my $clean_date = $now - $day_dur;
  253. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  254. log_info($dbh, "Cleaning syslog entries older than $clean_str");
  255. do_sql($dbh, "DELETE FROM remote_syslog WHERE ts < $clean_str");
  256. }
  257. # Clean old aggregated traffic statistics
  258. if ($history_trafstat_day) {
  259. my $day_dur = DateTime::Duration->new(days => $history_trafstat_day);
  260. my $clean_date = $now - $day_dur;
  261. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  262. log_info($dbh, "Cleaning traffic statistics older than $clean_str");
  263. do_sql($dbh, "DELETE FROM user_stats WHERE ts < $clean_str");
  264. }
  265. # Clean old per-IP full traffic statistics (if retention is configured)
  266. my $iptraf_history = $config_ref{traffic_ipstat_history};
  267. if ($iptraf_history) {
  268. my $day_dur = DateTime::Duration->new(days => $iptraf_history);
  269. my $clean_date = $now - $day_dur;
  270. my $clean_str = $dbh->quote($clean_date->ymd("-") . " 00:00:00");
  271. log_info($dbh, "Cleaning full traffic statistics older than $clean_str");
  272. do_sql($dbh, "DELETE FROM user_stats_full WHERE ts < $clean_str");
  273. }
  274. # Clean dangling user_auth records (deleted, but with no MAC — likely artifacts)
  275. do_sql($dbh, "DELETE FROM user_auth WHERE (mac IS NULL OR mac = '') AND deleted = 1");
  276. # Ensure current user locations are recorded in mac_history
  277. my %connections;
  278. my @connections_list = get_records_sql($dbh, "SELECT * FROM connections ORDER BY auth_id");
  279. foreach my $connection (@connections_list) {
  280. next if (!$connection);
  281. $connections{$connection->{auth_id}} = $connection;
  282. }
  283. # Build a set of currently active, non-empty MACs with valid connections
  284. my $auth_sql = "SELECT * FROM user_auth WHERE mac IS NOT NULL AND mac != '' AND deleted = 0 ORDER BY last_found DESC";
  285. my %auth_table;
  286. my @auth_full_list = get_records_sql($dbh, $auth_sql);
  287. foreach my $auth (@auth_full_list) {
  288. next if (!$auth);
  289. my $auth_mac = mac_simplify($auth->{mac});
  290. next if (exists $auth_table{$auth_mac});
  291. next if (!exists $connections{$auth->{id}});
  292. $auth_table{$auth_mac} = 1;
  293. # Check if location history already exists
  294. my $h_sql = "SELECT * FROM mac_history WHERE mac = '$auth_mac' ORDER BY ts";
  295. my $history = get_record_sql($dbh, $h_sql);
  296. my $cur_conn = $connections{$auth->{id}};
  297. if (!$history) {
  298. # First-time location: insert new history record
  299. my $new;
  300. $new->{device_id} = $cur_conn->{device_id};
  301. $new->{port_id} = $cur_conn->{port_id};
  302. $new->{auth_id} = $auth->{id};
  303. $new->{ip} = $auth->{ip};
  304. $new->{mac} = $auth_mac;
  305. $new->{ts} = $auth->{mac_found};
  306. db_log_info($dbh, "Auth id: $auth->{id} ($auth_mac) found at location: device_id=$new->{device_id}, port_id=$new->{port_id}");
  307. insert_record($dbh, "mac_history", $new);
  308. next;
  309. }
  310. # Check if location has changed since last history entry
  311. if ($history->{device_id} != $cur_conn->{device_id} || $history->{port_id} != $cur_conn->{port_id}) {
  312. my $new;
  313. $new->{device_id} = $cur_conn->{device_id};
  314. $new->{port_id} = $cur_conn->{port_id};
  315. $new->{auth_id} = $auth->{id};
  316. $new->{ip} = $auth->{ip};
  317. $new->{mac} = $auth_mac;
  318. $new->{ts} = $auth->{mac_found};
  319. db_log_info($dbh, "Auth id: $auth->{id} ($auth_mac) moved to new location: device_id=$new->{device_id}, port_id=$new->{port_id}");
  320. insert_record($dbh, "mac_history", $new);
  321. }
  322. }
  323. # Optional: Optimize database tables to reclaim space and improve performance
  324. #if ($optimize) {
  325. # log_info($dbh, 'Starting table optimization');
  326. # foreach my $table (@db_tables) {
  327. # my $opt_sql = "OPTIMIZE TABLE $table";
  328. # my $opt_rf = $dbh->prepare($opt_sql) or die "Unable to prepare $opt_sql: " . $dbh->errstr;
  329. # $opt_rf->execute();
  330. # # Alternative (manual rebuild) is descriptioned out:
  331. # # CREATE TABLE $table.new LIKE $table;
  332. # # INSERT INTO $table.new SELECT * FROM $table;
  333. # # RENAME TABLE $table TO $table.backup, $table.new TO $table;
  334. # # DROP TABLE $table.backup;
  335. # }
  336. # log_info($dbh, 'Table optimization completed.');
  337. #}
  338. log_info($dbh, 'Garbage collection finished.');
  339. $dbh->disconnect;
  340. exit 0;