
Иногда возникает желание мониторить локальный GIT сервер на предмет кто (ФИО из LDAP), какой проект и откуда(ip-адрес) клонит или пушит.
Изучив документацию, стало ясно, что такого функционала из коробки нет, точнее есть, но в платной версии GitLab. Под катом мой опыт реализации мониторинга.
Мой рецепт не претендует на универсальность, я надеюсь он многим пригодится как, отправная точка.
Описание моей конфигурации:
У нас установлен «GitLab Community Edition 10.0.3», авторизация пользователей происходит по LDAP, клон и пуш они делают используя единую SSH учетку 'git'. По сути это стандартная конфигурация для более или менее крупной компании.
При каждом git-clone и git-push в файле '/var/log/auth.log' появляется сообщение о том, что пользователь «git » авторизовался в системе с таким-то 'fingerprint'
cat auth.log
Oct 17 12:41:11 GitLab-test sshd[25931]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.111.24 user=git Oct 17 12:41:13 GitLab-test sshd[25931]: Accepted publickey for git from 192.168.111.24 port 55268 ssh2: RSA 63:3b:ca:8d:23:2f:d2:0c:40:ce:4d:2e:b1:2e:5f:7c Oct 17 12:41:13 GitLab-test sshd[25931]: pam_unix(sshd:session): session opened for user git by (uid=0)
Затем в файле '/var/log/gitlab/gitlab-shell/gitlab-shell.log' появляется сообщение о том, что пользователь с таким-то 'key' склонировал или спушил такой-то проэкт.
cat gitlab-shell.log
I, [2017-10-19T16:17:32.006429 #1115] INFO -- : POST http://127.0.0.1:8080/api/v4/internal/allowed 0.02417 I, [2017-10-19T16:17:32.006954 #1115] INFO -- : gitlab-shell: executing git command <git-upload-pack /var/opt/gitlab/git-data/repositories/tech/ansible-server.git> for user with key key-1030.
И наконец в файле '/var/log/auth.log' появляется сообщение о том, что пользователь 'git ' вышел из системы:
cat auth.log
Oct 17 12:41:13 GitLab-test sshd[25944]: Received disconnect from 192.168.111.24: 11: disconnected by user Oct 17 12:41:13 GitLab-test sshd[25931]: pam_unix(sshd:session): session closed for user git
Изучив все таблицы в базе данных стало ясно, что по мифическому 'key', который есть в логах GitLab`a, можно найти вменяемое имя пользователя и его 'fingerprint'.
Т.к. в логах строчки появляются в строгом порядке, то самая последняя запись в '/var/log/auth.log' с нужным ��ам 'fingerprint' будет содержать IP-адрес пользователя. Даже если сообщения от разных пользователей будут записываться не строго по порядку, сбоя не произойдет т.к. соответствие 'fingerprint' — 'IP-адрес' ищется с конца.
Имя пользователя находится в таблице 'identities' по 'user_id', который можно найти в таблице 'keys' по 'key' который мы видим в лог файле 'gitlab-shell.log'.
SELECT extern_uid FROM identities WHERE user_id = (SELECT user_id FROM keys WHERE id = 1030);
В таблице 'keys' по 'key' находится 'fingerprint'
SELECT fingerprint FROM keys WHERE id = 1030;
SQL запрос для поиска реального имени пользователя:
SELECT extern_uid FROM identities WHERE user_id = (SELECT user_id FROM keys WHERE id = 1030);
Воорижившись этими данными был придуман алгоритм, как найти имя пользователя, его ip-адрес, проект и действие которое он совершил.
1. парсим с помощью tail -f новые строчки в логе гита,
2. как только попадается строчка соответствующия регулярке идем в базу данный и ищем по полученому 'key' имя пользователя и его 'fingerprint',
3. по 'fingerprint' в 'auth.log' ищем ip-адрес пользователя и берем самую свежую запись.
Скрипт написан на питоне(Пайтоне, да простят меня фанаты).
Это мой первый опыт написания чего-либо на питоне. Буду признателен за конструктивную критику и рекомендации.
Для его работы необходимы следующие библиотеки и модули:
python-tail Для отслеживания новых строчек в файле 'gitlab-shell.log'
psycopg Для работы с PostgreSQL
gelfHandler Для отправки сообщений в GrayLog сервер
2. как только попадается строчка соответствующия регулярке идем в базу данный и ищем по полученому 'key' имя пользователя и его 'fingerprint',
3. по 'fingerprint' в 'auth.log' ищем ip-адрес пользователя и берем самую свежую запись.
Скрипт написан на питоне(Пайтоне, да простят меня фанаты).
Это мой первый опыт написания чего-либо на питоне. Буду признателен за конструктивную критику и рекомендации.
Для его работы необходимы следующие библиотеки и модули:
python-tail Для отслеживания новых строчек в файле 'gitlab-shell.log'
psycopg Для работы с PostgreSQL
gelfHandler Для отправки сообщений в GrayLog сервер
Скрипт получился довольно большой, он умеет выдергивать из конфигурационного файла 'gitlab.rb' данный для подключения к базе PostgreSQL, проверяет наличие лог-файлов SSH и 'gitlab-shell.log', умеет писать результат в файл и отправлять в GreyLog сервер, пишет логи об ошибках в файл и в консоль. Любой из этих параметров можно включить или отключить.
Скрипт
#!/usr/bin/python # -*- coding: utf-8 -*- """ Краткое описание: 1. В '/var/log/gitlab/gitlab-shell/gitlab-shell.log' ищем имя проэкта, действие и 'key' юзера. 2. В базе данных ищем реальное имя пользователя и его 'fingerprint' по найденому ранее 'key'. 3. В "/var/log/auth.log" ищем IP пользователя в самой последней строчке, с нужным нам 'fingerprint'. Строчка из gitlab-shell.log I, [2017-10-17T12:19:56.526131 #21521] INFO -- : gitlab-shell: executing git command <git-receive-pack /var/opt/gitlab/git-data/repositories/web/markets.git> for user with key key-11. SQL запрос для поиска 'fingerprint' SELECT fingerprint FROM keys WHERE id = 11; SQL запрос для поиска реального имени пользователя: SELECT extern_uid FROM identities WHERE user_id = (SELECT user_id FROM keys WHERE id = 11); """ # Импортируем модули и библиотеки from gelfHandler import GelfHandler import logging import psycopg2 import re import os import tail import subprocess import sys import datetime time=str(datetime.datetime.now()) # Включить/включить дебаг-on или отключить - off debug = 'on' #debug = '' # Включить/включить отображение полного пути до файла GIT. Имя файла соответствует имени проекта. #pach_git_all = 'on' pach_git_all = 'off' # Пути к лог-файлам: log_file_GuiLab = '/var/log/gitlab/gitlab-shell/gitlab-shell.log' log_file_SSH = '/var/log/auth.log' # Параметры подключения к SQL. Для автоматического определения параметров, необходимо указать путь до конфигурационного файла гитлаба, либо указать свои параметры. gitlab_rb = '/etc/gitlab/gitlab.rb' #gitlab_rb = '' sql_db = 'gitlab' sql_user = 'python_user' sql_password = 'qwer123' sql_host = '127.0.0.1' sql_port = '5432' # Параметры грейлог сервера: out_GrayLog='yes' #out_GrayLog='' logger = logging.getLogger() gelfHandler = GelfHandler( host='192.168.250.145', port=6514, protocol='UDP', facility='Python_parsing_log_file_GitLab' ) logger.addHandler(gelfHandler) #параметры записи результатов в логфайл out_log_file_name='/home/viktor/pars_log_GitLab.log' #out_log_file_name='' # Функция обработки ошибки при отсутствии лог-файлов def funk_error_file(log_file): print "Файл ", log_file, "не найден!!!" out_log_file = open(out_log_file_name, 'a') out_log_file.write(time); out_log_file.write(" "); out_log_file.write("Файл "); out_log_file.write(log_file); out_log_file.write(" не найден!!!"); out_log_file.close() sys.exit() # Весь скрипт это одна функция tail, она начинает выполняться, когда в файле "gitlab-shell.log" появляется новая строчка def funk_pars_gitlab_shell(string_gitlab_shell): # Локальные переменные: action = 'action' project = 'project' user_key = 0 username = 'username' time_ssh = 'time_ssh' host_name = 'host_name' id_ssh_log_message = 0 usr_name_git = 'usr_name_git' ip_address = 'ip_address' fingerprint_log_ssh = 'fingerprint_log_ssh' fingerprint = 'fingerprint' time_git = 'time_git' id_git_log_message = 'id_git_log_message' # Проверяем регуляркой, чтоб новая строчка соответствовала шаблону, в результате получаем массив с тремя элементами, в том числе и пустыми(предварительно проверка на полный и короткий путь до файла) if pach_git_all == 'on': regexp_string_gitlab_shell = re.findall(r'^.*\[([^ ]+)\.[\d]+\s\#([^ ]+)\]\s.*<([^ ]*)\s([^ ]*)>\s{1,}for user with key\s{1,}key-([^ ]*)\.$', string_gitlab_shell) elif pach_git_all == 'off': regexp_string_gitlab_shell = re.findall(r'^.*\[([^ ]+)\.[\d]+\s\#([^ ]+)\]\s.*<([^ ]*)\s.+repositories/([^ ]*)\.git>\s{1,}for user with key\s{1,}key-([^ ]*)\.$', string_gitlab_shell) # Выдергиваем из полученного массива переменные for arr_string__gitlab_shell in regexp_string_gitlab_shell: time_git = arr_string__gitlab_shell[0] # Время записи сообщения в лог-файл GIT id_git_log_message = arr_string__gitlab_shell[1] # Идентификатор сообщения в лог-файле GIT action = arr_string__gitlab_shell[2] # Действие т.е. клон, пуш или что-то там ещё. project = arr_string__gitlab_shell[3] # Проэкт в который клонили или пушили user_key = arr_string__gitlab_shell[4] # Некий индетификатор пользователя присвоеный ему гитлабом. # Проверим есть ли в переменной новые данные дабы не дергать лишний раз базу данный и не присылать пустоту if action != 'action': # Подключение к базе данных connect = psycopg2.connect(database=sql_db, user=sql_user, password=sql_password, host=sql_host, port=sql_port) # Открываем курсор (т.е. подключаемя к базе данных) curs = connect.cursor() # Формируем переменную для подстановки в SQL запрос. В запросе по идентификатору пользователя будем искать его user_id, по которому еже найдем вменяемое имя пользователя sql_string_find_username="""SELECT extern_uid FROM identities WHERE user_id = (SELECT user_id FROM keys WHERE id = %s);""" %user_key curs.execute(sql_string_find_username) # сам запрос string_external_uid = curs.fetchall() # Массив с результтаом запроса # Формируем переменную для подстановки в SQL запрос. В запросе по идентификатору пользователя будем искать его fingerprint. sql_string_find_fingerprint="""SELECT fingerprint FROM keys WHERE id = %s;""" %user_key curs.execute(sql_string_find_fingerprint) # сам запрос sql_string_fingerprint = curs.fetchall() # Массив с результтаом запроса # Закрываем соединение с базой connect.close() # В следующих двух циклах разбираем полученные массивы от SQL for fingerprint_array in sql_string_fingerprint: fingerprint = fingerprint_array[0] for username_array in string_external_uid: username = re.findall(r'^.*uid=([-\w\._]+)', username_array[0]) username = username[0] # Формируем переменную и делаем системный вызов для получения последней строчки из лог-файла auth.log с полученным из SQL fingerprint. command = """grep '%s' %s | tail -n 1""" %(fingerprint, log_file_SSH) p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for line in p.stdout.readlines(): str_line = str(line) retval = p.wait() # Разбираем найденную сторочку из auth.log при помощи регулярки на время, IP адрес и остальные не очень нужные данные.(имя сервера, идентификатор сообщения, SSH гит пользователь) arr_reg_exp_ssh_info = re.findall(r'^([\w*\s\d\:\s]+)\s([^ ]+)\ssshd\[(\d+)\]:\sAccepted publickey for\s([^ ]+)\sfrom\s([^ ]+)\sport\s[\s\w]+:\sRSA\s([\w\:]+)$', str_line) # Раскладываем массив по переменным for data_ssh_info in arr_reg_exp_ssh_info: time_ssh = data_ssh_info[0] host_name = data_ssh_info[1] id_ssh_log_message = data_ssh_info[2] usr_name_git = data_ssh_info[3] ip_address = data_ssh_info[4] fingerprint_log_ssh = data_ssh_info[5] # Изменим значение переменной 'action' на более привычные нам значения. if action == 'git-receive-pack': action = 'push' elif action == 'git-upload-pack': action = 'clone' # Печатаем переменные для отладки if debug: print '----' print ' ', 'time_ssh', '\t\t', time_ssh print ' ', 'time_git', '\t\t', time_git print ' ', 'username','\t\t', username print ' ', 'action', '\t\t', action print ' ', 'project', '\t\t', project print ' ', 'ip_address', '\t\t', ip_address print ' ', 'user_key', '\t\t', user_key print ' ', 'host_name', '\t\t', host_name print ' ', 'id_ssh_log_message','\t', id_ssh_log_message print ' ', 'id_git_log_message','\t', id_git_log_message print ' ', 'usr_name_git', '\t', usr_name_git print ' ', 'fingerprint_ssh', '\t', fingerprint_log_ssh print ' ', 'fingerprint_sql', '\t', fingerprint print '----' print '\n' # Формирование и отправка сообщения в грейлог if out_GrayLog != 'no': logger.warning( 'Now message', extra={'gelf_props': { 'title_time_ssh':time_ssh, 'title_time_git':time_git, 'title_username':username, 'title_action':action, 'title_project':project, 'title_ip_address':ip_address, 'title_user_key':user_key, 'title_host_name':host_name, 'title_id_ssh_log_message':id_ssh_log_message, 'title_id_git_log_message':id_git_log_message, 'title_fingerprint':fingerprint }}) # Формирование и отправка сообщения в файл if out_log_file_name: out_log_file = open(out_log_file_name, 'a') out_log_file.write("{time_ssh:"); out_log_file.write(time_ssh); out_log_file.write("}{time_git:"); out_log_file.write(time_git); out_log_file.write("}{username:"); out_log_file.write(username); out_log_file.write("}{action:"); out_log_file.write(action); out_log_file.write("}{project:"); out_log_file.write(project); out_log_file.write("}{ip_address:"); out_log_file.write(ip_address); out_log_file.write("}{user_key:"); out_log_file.write(user_key); out_log_file.write("}{host_name:"); out_log_file.write(host_name); out_log_file.write("}{id_ssh_log_message:"); out_log_file.write(id_ssh_log_message); out_log_file.write("}{id_git_log_message:"); out_log_file.write(id_git_log_message); out_log_file.write("}{fingerprint:"); out_log_file.write(fingerprint); out_log_file.write("}"); out_log_file.write("\n"); out_log_file.close() # Выполним проверку на наличие лог файлов, и корректно закроем скрипт если их нет(вызовом соответствующей функции) if os.path.exists(log_file_GuiLab): if os.path.exists(log_file_SSH): if gitlab_rb: command_searh_db = """grep "gitlab_rails\['db_" %s|tr -s '\\n' '\ '""" %gitlab_rb p = subprocess.Popen(command_searh_db, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for line in p.stdout.readlines(): str_db_line = str(line) retval = p.wait() arr_db_con = re.findall(r'^.*db_database\'\]\s\=\s\"([^ ]+)\".*db_username\'\]\s\=\s\"([^ ]+)\".*db_password\'\]\s\=\s\"([^ ]+)\".*db_host\'\]\s\=\s\"([^ ]+)\".*db_port\'\]\s\=\s([^ ]+)\s.*$', str_db_line) # Раскладываем массив по переменным for data_db_info in arr_db_con: sql_db = data_db_info[0] sql_user = data_db_info[1] sql_password = data_db_info[2] sql_host = data_db_info[3] sql_port = data_db_info[4] # Если включен дебаг, выводим шапку при запуске if debug: print '\n' print 'debug ==> on' print 'Start', (os.path.basename(__file__)) print '\n' print 'sql_db ==> ', sql_db print 'sql_user ==> ', sql_user print 'sql_password ==> ', sql_password print 'sql_host ==> ', sql_host print 'sql_port ==> ', sql_port print '----' # Запускаем главную функцию t = tail.Tail(log_file_GuiLab) # 'log_file_GuiLab' - Файл который будем парсить тайлом t.register_callback(funk_pars_gitlab_shell) # Вызов сомой функции 'funk_pars_gitlab_shell' t.follow(s=1) # Частота парсинга файла funk_error_file(log_file_SSH) funk_error_file(log_file_GuiLab)
И как логическое завершение service для systemd:
cat /lib/systemd/system/pars_log_GitLab.service
[Unit] Description=Python parsing_log_file_GitLab # стартовать после запуска следующих сервисов #After=network.target postgresql.service # Требуемые сервисы #Requires=postgresql.service # Необходимые сервисы #Wants=postgresql.service [Service] # Тип запуска Type=simple # Перезапуск при сбое Restart=always # расположение PID файла PIDFile=/var/run/appname/appname.pid # Рабочий каталог #WorkingDirectory=/home/username/appname # Пользователь и группа из под которых запускать User=root Group=root # Данный параметр необходим что бы дать права на выполнение следующих #PermissionsStartOnly=true # ExecStartPre - выполнить ДО старта приложения #ExecStartPre=-/usr/bin/mkdir -p /var/run/appname #ExecStartPre=/usr/bin/chown -R app_user:app_user_group /var/run/appname # Запуск приложения ExecStart=/usr/bin/python2 /usr/bin/pars_log_GitLab.py & # Пауза при необходимости TimeoutSec=300 [Install] WantedBy=multi-user.target
Теперь скриптом можно управлять следующими командами:
systemctl start pars_log_GitLab.service systemctl status pars_log_GitLab.service systemctl stop pars_log_GitLab.service
Не забудьте отключить дебаг перед запуском.
Всем спасибо за внимание!
