garbage.pl 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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},$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_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. my $ip_subnet = $dhcp_networks->match_string($row->{ip});
  133. if ($ip_subnet) {
  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 * $ip_subnet->{dhcp_lease_time};
  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 = $clean_date->ymd("-") . " 00:00:00";
  155. db_log_info($dbh, 'Clearing outdated DHCP log records');
  156. do_sql($dbh, "DELETE FROM dhcp_log WHERE ts < ?",$clean_str);
  157. db_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. db_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 = $clean_date->ymd("-") . " 00:00:00";
  165. $users_sql = "SELECT id FROM user_auth WHERE last_found < ? AND last_found IS NOT NULL";
  166. @users_auth = get_records_sql($dbh, $users_sql, $clean_str);
  167. foreach my $row (@users_auth) {
  168. log_debug( "Clearing old connection for auth_id: " . $row->{id});
  169. do_sql($dbh, "DELETE FROM connections WHERE auth_id = ?", $row->{id});
  170. }
  171. }
  172. # Remove duplicate connection records (same auth_id + port_id)
  173. db_log_info($dbh, 'Clearing duplicate connection records');
  174. my $conn_sql = "SELECT id, port_id, auth_id FROM connections ORDER BY port_id";
  175. my @conn_ref = get_records_sql($dbh, $conn_sql);
  176. my $old_port_id = 0;
  177. my $old_auth_id = 0;
  178. foreach my $row (@conn_ref) {
  179. my $c_id = $row->{id};
  180. my $c_port_id = $row->{port_id} || 0;
  181. my $c_auth_id = $row->{auth_id} || 0;
  182. if ($old_port_id == 0 || $old_auth_id == 0) {
  183. $old_port_id = $c_port_id;
  184. $old_auth_id = $c_auth_id;
  185. next;
  186. }
  187. # If we're still on the same (port, auth) pair, this is a duplicate
  188. if ($old_port_id == $c_port_id && $old_auth_id == $c_auth_id) {
  189. do_sql($dbh, "DELETE FROM connections WHERE id = ?",$c_id);
  190. db_log_info($dbh, "Removed duplicate connection id=$c_id: port=$c_port_id auth=$c_auth_id");
  191. } else {
  192. $old_port_id = $c_port_id;
  193. $old_auth_id = $c_auth_id;
  194. }
  195. }
  196. # Clean empty dynamic/hotspot user accounts (no active authentications)
  197. db_log_info($dbh, 'Clearing empty user accounts and associated devices for dynamic users and hotspot');
  198. 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";
  199. my @u_ref = get_records_sql($dbh, $u_sql, $default_hotspot_ou_id, $default_user_ou_id);
  200. foreach my $row (@u_ref) {
  201. db_log_info($dbh, "Removing empty dynamic user with id: $row->{id}, login: $row->{login}");
  202. delete_user($dbh, $row->{id});
  203. }
  204. # Clean empty non-permanent user accounts (if enabled in config)
  205. if ($config_ref{clean_empty_user}) {
  206. db_log_info($dbh, 'Clearing empty non-permanent user accounts and associated devices');
  207. 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;";
  208. my @u_ref = get_records_sql($dbh, $u_sql);
  209. foreach my $row (@u_ref) {
  210. db_log_info($dbh, "Removing empty user with id: $row->{id}, login: $row->{login}");
  211. delete_user($dbh, $row->{id});
  212. }
  213. }
  214. # Remove orphaned auth rules (no corresponding user)
  215. do_sql($dbh, "DELETE FROM auth_rules WHERE user_id NOT IN (SELECT id FROM user_list)");
  216. # Clean unknown MAC entries that now belong to known users
  217. db_log_info($dbh, 'Removing unknown MAC records that are now associated with active users');
  218. $users_sql = "SELECT mac FROM user_auth WHERE deleted = 0";
  219. @users_auth = get_records_sql($dbh, $users_sql);
  220. foreach my $row (@users_auth) {
  221. next if (!$row->{mac});
  222. do_sql($dbh, "DELETE FROM unknown_mac WHERE mac = ?", mac_splitted($row->{mac}));
  223. }
  224. # Clean old detailed traffic records (based on global $history setting)
  225. if ($history) {
  226. my $day_dur = DateTime::Duration->new(days => $history);
  227. my $clean_date = $now - $day_dur;
  228. my $clean_str = $clean_date->ymd("-") . " 00:00:00";
  229. db_log_info($dbh, "Cleaning traffic detail records older than $clean_str");
  230. do_sql($dbh, "DELETE FROM traffic_detail WHERE ts < ?", $clean_str);
  231. }
  232. # Clean verbose (non-info) worklog entries
  233. if ($history_log_day) {
  234. my $day_dur = DateTime::Duration->new(days => $history_log_day);
  235. my $clean_date = $now - $day_dur;
  236. my $clean_str = $clean_date->ymd("-") . " 00:00:00";
  237. db_log_info($dbh, "Cleaning VERBOSE worklog entries older than $clean_str");
  238. do_sql($dbh, "DELETE FROM worklog WHERE level > ? AND ts < ?", $L_INFO, $clean_str);
  239. }
  240. # Clean debug-level worklog entries older than $debug_history days (hardcoded to 3)
  241. if ($debug_history) {
  242. my $day_dur = DateTime::Duration->new(days => 3);
  243. my $clean_date = $now - $day_dur;
  244. my $clean_str = $clean_date->ymd("-") . " 00:00:00";
  245. db_log_info($dbh, "Cleaning debug worklog entries older than $clean_str");
  246. do_sql($dbh, "DELETE FROM worklog WHERE level >= ? AND ts < ?",$L_DEBUG, $clean_str);
  247. }
  248. # Clean old remote syslog entries
  249. if ($history_syslog_day) {
  250. my $day_dur = DateTime::Duration->new(days => $history_syslog_day);
  251. my $clean_date = $now - $day_dur;
  252. my $clean_str = $clean_date->ymd("-") . " 00:00:00";
  253. db_log_info($dbh, "Cleaning syslog entries older than $clean_str");
  254. do_sql($dbh, "DELETE FROM remote_syslog WHERE ts < ?",$clean_str);
  255. }
  256. # Clean old aggregated traffic statistics
  257. if ($history_trafstat_day) {
  258. my $day_dur = DateTime::Duration->new(days => $history_trafstat_day);
  259. my $clean_date = $now - $day_dur;
  260. my $clean_str = $clean_date->ymd("-") . " 00:00:00";
  261. db_log_info($dbh, "Cleaning traffic statistics older than $clean_str");
  262. do_sql($dbh, "DELETE FROM user_stats WHERE ts < ?",$clean_str);
  263. }
  264. # Clean old per-IP full traffic statistics (if retention is configured)
  265. my $iptraf_history = $config_ref{traffic_ipstat_history};
  266. if ($iptraf_history) {
  267. my $day_dur = DateTime::Duration->new(days => $iptraf_history);
  268. my $clean_date = $now - $day_dur;
  269. my $clean_str = $clean_date->ymd("-") . " 00:00:00";
  270. db_log_info($dbh, "Cleaning full traffic statistics older than $clean_str");
  271. do_sql($dbh, "DELETE FROM user_stats_full WHERE ts < ?",$clean_str);
  272. }
  273. # Clean dangling user_auth records (deleted, but with no MAC — likely artifacts)
  274. do_sql($dbh, "DELETE FROM user_auth WHERE (mac IS NULL OR mac = '') AND deleted = 1");
  275. # Ensure current user locations are recorded in mac_history
  276. my %connections;
  277. my @connections_list = get_records_sql($dbh, "SELECT * FROM connections ORDER BY auth_id");
  278. foreach my $connection (@connections_list) {
  279. next if (!$connection);
  280. $connections{$connection->{auth_id}} = $connection;
  281. }
  282. # Build a set of currently active, non-empty MACs with valid connections
  283. my $auth_sql = "SELECT * FROM user_auth WHERE mac IS NOT NULL AND mac != '' AND deleted = 0 ORDER BY last_found DESC";
  284. my %auth_table;
  285. my @auth_full_list = get_records_sql($dbh, $auth_sql);
  286. foreach my $auth (@auth_full_list) {
  287. next if (!$auth);
  288. my $auth_mac = mac_splitted($auth->{mac});
  289. next if (exists $auth_table{$auth_mac});
  290. next if (!exists $connections{$auth->{id}});
  291. $auth_table{$auth_mac} = 1;
  292. # Check if location history already exists
  293. my $h_sql = "SELECT * FROM mac_history WHERE mac = ? ORDER BY ts";
  294. my $history = get_record_sql($dbh, $h_sql, $auth_mac);
  295. my $cur_conn = $connections{$auth->{id}};
  296. if (!$history) {
  297. # First-time location: insert new history record
  298. my $new;
  299. $new->{device_id} = $cur_conn->{device_id};
  300. $new->{port_id} = $cur_conn->{port_id};
  301. $new->{auth_id} = $auth->{id};
  302. $new->{ip} = $auth->{ip};
  303. $new->{mac} = $auth_mac;
  304. $new->{ts} = $auth->{mac_found};
  305. db_log_info($dbh, "Auth id: $auth->{id} ($auth_mac) found at location: device_id=$new->{device_id}, port_id=$new->{port_id}");
  306. insert_record($dbh, "mac_history", $new);
  307. next;
  308. }
  309. # Check if location has changed since last history entry
  310. if ($history->{device_id} != $cur_conn->{device_id} || $history->{port_id} != $cur_conn->{port_id}) {
  311. my $new;
  312. $new->{device_id} = $cur_conn->{device_id};
  313. $new->{port_id} = $cur_conn->{port_id};
  314. $new->{auth_id} = $auth->{id};
  315. $new->{ip} = $auth->{ip};
  316. $new->{mac} = $auth_mac;
  317. $new->{ts} = $auth->{mac_found};
  318. db_log_info($dbh, "Auth id: $auth->{id} ($auth_mac) moved to new location: device_id=$new->{device_id}, port_id=$new->{port_id}");
  319. insert_record($dbh, "mac_history", $new);
  320. }
  321. }
  322. # Optional: Optimize database tables to reclaim space and improve performance
  323. #if ($optimize) {
  324. # db_log_info($dbh, 'Starting table optimization');
  325. # foreach my $table (@db_tables) {
  326. # my $opt_sql = "OPTIMIZE TABLE $table";
  327. # my $opt_rf = $dbh->prepare($opt_sql) or die "Unable to prepare $opt_sql: " . $dbh->errstr;
  328. # $opt_rf->execute();
  329. # # Alternative (manual rebuild) is descriptioned out:
  330. # # CREATE TABLE $table.new LIKE $table;
  331. # # INSERT INTO $table.new SELECT * FROM $table;
  332. # # RENAME TABLE $table TO $table.backup, $table.new TO $table;
  333. # # DROP TABLE $table.backup;
  334. # }
  335. # db_log_info($dbh, 'Table optimization completed.');
  336. #}
  337. db_log_info($dbh, 'Garbage collection finished.');
  338. $dbh->disconnect;
  339. exit 0;