stat-sync.pl 12 KB

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