Мониторинг событий git clone и git push на локальном GitLab сервере +1

- такой же как Forbes, только лучше.


Иногда возникает желание мониторить локальный 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 сервер

Скрипт получился довольно большой, он умеет выдергивать из конфигурационного файла '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

Не забудьте отключить дебаг перед запуском.

Всем спасибо за внимание!

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (9):

  1. SLASH_CyberPunk
    /#10490178 / +1

    Заголовок и теги поменяйте/впишите GitLab. Все таки у вас речь идет именно про него и только!

  2. manefesto
    /#10490318

    Мне не совсем понятен этот момент

    #		Формирование и отправка сообщения в файл
    		if out_log_file_name:
    

    Я так понимаю вы формируете что-то вроде json?
    Разве нельзя нормально писать через модуль json
    json.dump(data,file)


    или на худой конец писать в БД

  3. Ash666
    /#10490338

    Мне не совсем понятен этот момент

    # Формирование и отправка сообщения в файл
    if out_log_file_name:

    Проверка есть ли в переменной значение, если она пустая, то в файл результат писаться не будет. По поводу json — ничего не формирую, изначально задача стаяла отправлять всё в GreyLog, отправку в файл сделал только ради этой статьи.

    • Ash666
      /#10490344

      Мне не совсем понятен этот момент

      # Формирование и отправка сообщения в файл
      if out_log_file_name:

      Проверка есть ли в переменной значение, если она пустая, то в файл результат писаться не будет. По поводу json — ничего не формирую, изначально задача стаяла отправлять всё в GreyLog, отправку в файл сделал только ради этой статьи.

  4. GHostly_FOX
    /#10490504

    Я для такого мониторинга использую просто интеграцию по Slack

  5. lex-tsy
    /#10492872

    При установке через omnibus, конфигурация подключения к postgresql немного иная.

    /opt/gitlab/embedded/bin/psql --port 5432 -h /var/opt/gitlab/postgresql -d gitlabhq_production