stat-sync.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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 Data::Dumper;
  15. use eyelib::config;
  16. use eyelib::main;
  17. use eyelib::database;
  18. use eyelib::common;
  19. use eyelib::net_utils;
  20. use strict;
  21. use Getopt::Long;
  22. use Proc::Daemon;
  23. use Cwd;
  24. use Net::Netmask;
  25. use DateTime;
  26. my $mute_time=300;
  27. my $pf = '/run/eye/stat-sync.pid';
  28. my $daemon = Proc::Daemon->new(
  29. pid_file => $pf,
  30. work_dir => $HOME_DIR
  31. );
  32. # are you running? Returns 0 if not.
  33. my $pid = $daemon->Status($pf);
  34. my $daemonize = 1;
  35. GetOptions(
  36. 'daemon!' => \$daemonize,
  37. "help" => \&usage,
  38. "reload" => \&reload,
  39. "restart" => \&restart,
  40. "start" => \&run,
  41. "status" => \&status,
  42. "stop" => \&stop
  43. ) or &usage;
  44. exit(0);
  45. ### Analyze DHCP requests
  46. # === 1. Получение событий из очереди DHCP ===
  47. sub _fetch_dhcp_queue {
  48. my ($dbh) = @_;
  49. my @events = get_records_sql($dbh, "SELECT * FROM dhcp_queue");
  50. log_debug("Fetched " . scalar(@events) . " DHCP event(s) from queue") if @events;
  51. return @events;
  52. }
  53. # === 2. Проверка, нужно ли подавлять событие (mute logic) ===
  54. sub _should_mute_dhcp_event {
  55. my ($ip, $new_action, $leases_ref, $mute_time) = @_;
  56. return 0 unless exists $leases_ref->{$ip};
  57. my $last_event = $leases_ref->{$ip};
  58. my $time_diff = time() - ($last_event->{last_time} // 0);
  59. # Подавляем, если:
  60. # - действие не отличается от предыдущего
  61. # - и прошло меньше $mute_time секунд
  62. if ($last_event->{action} eq $new_action && $time_diff <= $mute_time) {
  63. log_debug("Muting DHCP event for IP $ip: same as recent opposite action (diff: ${time_diff}s)");
  64. return 1;
  65. }
  66. return 0;
  67. }
  68. # === 3. Обработка одного DHCP-события ===
  69. sub _process_single_dhcp_event {
  70. my ($dbh, $event, $leases_ref, $mute_time) = @_;
  71. my $ip = $event->{ip};
  72. my $action = $event->{action};
  73. # Проверка на подавление
  74. if (_should_mute_dhcp_event($ip, $action, $leases_ref, $mute_time)) {
  75. # Всё равно обновляем last_time, чтобы не накапливать старые записи
  76. $leases_ref->{$ip} = $event;
  77. $leases_ref->{$ip}->{last_time} //= time();
  78. return;
  79. }
  80. # Обработка запроса
  81. log_debug("Processing DHCP event: action=$action, ip=$ip, mac=$event->{mac}");
  82. my $dhcp_record = process_dhcp_request(
  83. $dbh,
  84. $action,
  85. $event->{mac},
  86. $ip,
  87. $event->{dhcp_hostname},
  88. '', '', ''
  89. );
  90. # Удаляем из очереди
  91. my $rows = do_sql($dbh, "DELETE FROM dhcp_queue WHERE id = ?", $event->{id});
  92. log_debug("Deleted DHCP event ID $event->{id} from queue (affected rows: $rows)");
  93. # Проверяем, что запись создана/обновлена
  94. if (!$dhcp_record or !$dhcp_record->{auth_id} ){
  95. log_error("User ip auth record not created by DHCP request for ip: $ip mac: $event->{mac}!");
  96. return;
  97. }
  98. # Обновляем кэш последних событий
  99. $leases_ref->{$ip} = $event;
  100. $leases_ref->{$ip}->{last_time} //= time();
  101. }
  102. # === 4. Основная функция обработки очереди DHCP ===
  103. sub process_dhcp_queue {
  104. my ($hdb, $leases_ref, $mute_time) = @_;
  105. # Получаем все события
  106. my @dhcp_events = _fetch_dhcp_queue($hdb);
  107. return unless @dhcp_events;
  108. log_info("Processing " . scalar(@dhcp_events) . " DHCP event(s) from queue");
  109. # Обрабатываем каждое событие
  110. foreach my $dhcp (@dhcp_events) {
  111. eval {
  112. _process_single_dhcp_event($hdb, $dhcp, $leases_ref, $mute_time);
  113. };
  114. if ($@) {
  115. log_error("Failed to process DHCP event ID $dhcp->{id}: $@");
  116. # Не прерываем остальные события
  117. }
  118. }
  119. log_info("DHCP queue processing completed");
  120. }
  121. ### UPDATE user state
  122. # === 1. Обнуление флагов changed для динамических/хостспот-пользователей ===
  123. sub _reset_changed_flags_for_default_ous {
  124. my ($dbh, $default_user_ou_id, $default_hotspot_ou_id) = @_;
  125. if (!defined $default_user_ou_id || !defined $default_hotspot_ou_id) {
  126. log_warning("Skipping reset of changed flags: default OU IDs not set");
  127. return;
  128. }
  129. my $rows1 = do_sql($dbh, "UPDATE user_auth SET changed = 0 WHERE ou_id = ? OR ou_id = ?", $default_user_ou_id, $default_hotspot_ou_id);
  130. my $rows2 = do_sql($dbh, "UPDATE user_auth SET dhcp_changed = 0 WHERE ou_id = ? OR ou_id = ?", $default_user_ou_id, $default_hotspot_ou_id);
  131. log_debug("Reset 'changed' flags for $rows1 records, 'dhcp_changed' for $rows2 records (default OUs)");
  132. }
  133. # === 2. Сброс флагов changed для IP вне офисных сетей ===
  134. sub _clear_changed_flags_for_non_office_ips {
  135. my ($dbh, $office_networks) = @_;
  136. my @all_changed = get_records_sql($dbh, "SELECT id, ip FROM user_auth WHERE changed = 1 OR dhcp_changed = 1");
  137. return unless @all_changed;
  138. my $cleared = 0;
  139. for my $row (@all_changed) {
  140. next if !$row->{ip};
  141. next if $office_networks->match_string($row->{ip}); # IP в офисной сети — оставляем
  142. my $rows = do_sql($dbh, "UPDATE user_auth SET changed = 0, dhcp_changed = 0 WHERE id = ?", $row->{id});
  143. $cleared += $rows;
  144. }
  145. if ($cleared) {
  146. log_info("Cleared 'changed' flags for $cleared records with non-office IPs");
  147. }
  148. }
  149. # === 3. Обработка DHCP-изменений ===
  150. sub _process_dhcp_changes {
  151. my ($dbh) = @_;
  152. my $changed = get_record_sql($dbh, "SELECT COUNT(*) AS c_count FROM user_auth WHERE dhcp_changed = 1");
  153. my $count = $changed ? ($changed->{c_count} // 0) : 0;
  154. return if $count == 0;
  155. log_info("Found $count record(s) with dhcp_changed=1");
  156. # Сбрасываем флаги
  157. do_sql($dbh, "UPDATE user_auth SET dhcp_changed = 0");
  158. # Запускаем внешний скрипт
  159. my $dhcp_exec = get_option($dbh, 38);
  160. if (!$dhcp_exec) {
  161. log_warning("DHCP sync script (opt 38) not configured");
  162. return;
  163. }
  164. my %result = do_exec_ref("/usr/bin/sudo $dhcp_exec");
  165. if ($result{status} != 0) {
  166. log_error("DHCP config sync failed: " . ($result{stderr} // 'no error output'));
  167. } else {
  168. log_info("DHCP config synced successfully");
  169. }
  170. }
  171. # === 4. Обработка ACL-изменений ===
  172. sub _process_acl_changes {
  173. my ($dbh) = @_;
  174. my $changed = get_record_sql($dbh, "SELECT COUNT(*) AS c_count FROM user_auth WHERE changed = 1");
  175. my $count = $changed ? ($changed->{c_count} // 0) : 0;
  176. return if $count == 0;
  177. log_info("Found $count record(s) with changed=1 (ACL/DHCP)");
  178. my $acl_exec = get_option($dbh, 37);
  179. if (!$acl_exec) {
  180. log_warning("ACL sync script (opt 37) not configured");
  181. return;
  182. }
  183. my %result = do_exec_ref("$acl_exec --changes-only");
  184. if ($result{status} != 0) {
  185. log_error("Gateway ACL sync failed: " . ($result{stderr} // 'no error output'));
  186. } else {
  187. log_info("Gateway ACL synced successfully");
  188. }
  189. }
  190. # === 5. Обработка DNS-очереди ===
  191. sub _process_dns_queue {
  192. my ($dbh) = @_;
  193. my @dns_changed = get_records_sql($dbh, "SELECT DISTINCT auth_id FROM dns_queue");
  194. return unless @dns_changed;
  195. log_info("Processing DNS queue for " . scalar(@dns_changed) . " auth_id(s)");
  196. for my $auth (@dns_changed) {
  197. my $auth_id = $auth->{auth_id};
  198. eval {
  199. update_dns_record($dbh, $auth_id);
  200. do_sql($dbh, "DELETE FROM dns_queue WHERE auth_id = ?", $auth_id);
  201. log_info("DNS processed and cleared for auth_id: $auth_id");
  202. };
  203. if ($@) {
  204. log_error("Failed to process DNS for auth_id=$auth_id: $@");
  205. }
  206. }
  207. }
  208. # === 6. Очистка временных записей user_auth ===
  209. sub _cleanup_expired_dynamic_users {
  210. my ($dbh) = @_;
  211. # Используем параметризованный запрос вместо quote()
  212. my $now_str = DateTime->now(time_zone => 'local')->strftime('%Y-%m-%d %H:%M:%S');
  213. my @users_auth = get_records_sql($dbh,
  214. "SELECT id, user_id, end_life FROM user_auth WHERE deleted = 0 AND dynamic = 1 AND end_life <= ?",
  215. $now_str
  216. );
  217. return unless @users_auth;
  218. log_info("Cleaning up " . scalar(@users_auth) . " expired dynamic user_auth records");
  219. for my $row (@users_auth) {
  220. eval {
  221. delete_user_auth($dbh, $row->{id});
  222. db_log_info($dbh, "Removed dynamic user auth record for auth_id: $row->{id} by end_life time: $row->{end_life}", $row->{id});
  223. # Удаляем пользователя, если больше нет активных auth-записей
  224. my $u_count = get_count_records($dbh, 'user_auth', 'deleted = 0 AND user_id = ?', $row->{user_id});
  225. if ($u_count == 0) {
  226. delete_user($dbh, $row->{user_id});
  227. log_info("Deleted orphaned user_id: $row->{user_id}");
  228. }
  229. };
  230. if ($@) {
  231. log_error("Error cleaning up auth_id $row->{id}: $@");
  232. }
  233. }
  234. }
  235. # === 7. Основная функция обновления конфигурации ===
  236. sub refresh_config_if_needed {
  237. my ($hdb, $last_refresh_ref, $default_user_ou_id, $default_hotspot_ou_id, $office_networks) = @_;
  238. return if time() - $$last_refresh_ref < 60;
  239. log_debug("Starting config refresh cycle");
  240. # Обновляем опции
  241. init_option($hdb);
  242. my $urgent_sync = get_option($hdb, 50);
  243. if ($urgent_sync) {
  244. log_info("Urgent sync triggered (option 50)");
  245. _reset_changed_flags_for_default_ous($hdb, $default_user_ou_id, $default_hotspot_ou_id);
  246. _clear_changed_flags_for_non_office_ips($hdb, $office_networks);
  247. _process_dhcp_changes($hdb);
  248. _process_acl_changes($hdb);
  249. }
  250. _process_dns_queue($hdb);
  251. _cleanup_expired_dynamic_users($hdb);
  252. $$last_refresh_ref = time();
  253. log_debug("Config refresh cycle completed");
  254. }
  255. sub stop {
  256. if ($pid) {
  257. print "Stopping pid $pid...";
  258. if ($daemon->Kill_Daemon($pf)) {
  259. print "Successfully stopped.\n";
  260. } else {
  261. print "Could not find $pid. Was it running?\n";
  262. }
  263. } else {
  264. print "Not running, nothing to stop.\n";
  265. }
  266. }
  267. sub status {
  268. if ($pid) {
  269. print "Running with pid $pid.\n";
  270. } else {
  271. print "Not running.\n";
  272. }
  273. }
  274. sub run {
  275. if (!$pid) {
  276. print "Starting...";
  277. if ($daemonize) {
  278. # when Init happens, everything under it runs in the child process.
  279. # this is important when dealing with file handles, due to the fact
  280. # Proc::Daemon shuts down all open file handles when Init happens.
  281. # Keep this in mind when laying out your program, particularly if
  282. # you use filehandles.
  283. $daemon->Init;
  284. }
  285. setpriority(0,0,19);
  286. my %leases;
  287. while (1) {
  288. eval {
  289. # Create new database handle. If we can't connect, die()
  290. my $hdb = init_db();
  291. # Process DHCP queue every 10 seconds
  292. process_dhcp_queue($hdb, \%leases, $mute_time);
  293. # Update state every 60 seconds
  294. refresh_config_if_needed(
  295. $hdb,
  296. \$last_refresh_config,
  297. $default_user_ou_id,
  298. $default_hotspot_ou_id,
  299. $office_networks
  300. );
  301. sleep(10);
  302. };
  303. if ($@) { log_error("Exception found: $@"); sleep(300); }
  304. }
  305. } else {
  306. print "Already Running with pid $pid\n";
  307. }
  308. }
  309. sub usage {
  310. print "usage: stat-sync.pl (start|stop|restart)\n";
  311. exit(0);
  312. }
  313. sub reload {
  314. print "reload process not implemented.\n";
  315. }
  316. sub restart {
  317. stop;
  318. run;
  319. }