garbage.pl 15 KB

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