mirror of
https://github.com/munin-monitoring/contrib.git
synced 2018-11-08 00:59:34 +01:00
646a6c6985
The USER environment variable is not populated when the plugin is executed. Switched to checking the effective user.
290 lines
9.4 KiB
Python
Executable File
290 lines
9.4 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
|
|
"""=cut
|
|
=head1 NAME
|
|
|
|
git_commit_behind - Munin plugin to monitor local git repositories and report
|
|
how many commits behind their remote they are
|
|
|
|
=head1 NOTES
|
|
|
|
This plugin is similar to how apt_all works for apt packages.
|
|
|
|
To be able to check how behind a git repository is, we need to run git fetch.
|
|
To avoid fetching all repos every 5 minutes (for each munin period) and thus
|
|
slowing down the data collection, the git fetch operation is only randomly
|
|
triggered (based on env.update.probability).
|
|
In case of very time-consuming update operations, you can run them in a
|
|
separate cron job.
|
|
|
|
=head1 REQUIREMENTS
|
|
|
|
- Python3
|
|
- Git
|
|
|
|
=head1 INSTALLATION
|
|
|
|
Link this plugin, as usual.
|
|
For example :
|
|
ln -s /path/to/git_commit_behind /etc/munin/plugins/git_commit_behind
|
|
|
|
If you wish to update the repositories via cron and not during the plugin
|
|
execution (cf CONFIGURATION section), you need a dedicated cron job.
|
|
|
|
For example, you can use the following cron :
|
|
|
|
# If the git_commit_behind plugin is enabled, fetch git repositories randomly
|
|
# according to the plugin configuration.
|
|
# By default : once an hour (12 invocations an hour, 1 in 12 chance that the
|
|
# update will happen), but ensure that there will never be more than two hours
|
|
# (7200 seconds) interval between updates.
|
|
*/5 * * * * root if [ -x /etc/munin/plugins/git_commit_behind ]; then /usr/sbin/munin-run git_commit_behind update >/dev/null; fi
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
Use your "/etc/munin/plugin-conf.d/munin-node" to configure this plugin.
|
|
[git_commit_behind]
|
|
user [user]
|
|
env.git_path /path/to/git
|
|
env.update.mode [munin|cron]
|
|
env.update.probability 12
|
|
env.update.maxinterval 7200
|
|
|
|
user [user] : required, the owner of the repository checkouts
|
|
in case of multiple different owners, use root
|
|
env.git_path : optional (default : /usr/bin/git), the path to the git binary.
|
|
env.update.mode : optional (default : munin), the update mode.
|
|
munin : repositories are git fetched during the pugin execution
|
|
cron : a dedicated cron job needs to be used to update the repositories
|
|
env.update.probability : optional (default : 12),
|
|
runs the update randomly (1 in <probability> chances)
|
|
env.update.maxinterval : optional (default : 7200),
|
|
ensures that the update is run at least every <maxinterval> seconds
|
|
|
|
|
|
Then, for each repository you want to check, you need the following
|
|
configuration block under the git_commit_behind section
|
|
env.repo.[repoCode].path /path/to/local/repo
|
|
env.repo.[repoCode].name Repo Name
|
|
env.repo.[repoCode].user user
|
|
env.repo.[repoCode].warning 10
|
|
env.repo.[repoCode].critical 100
|
|
|
|
[repoCode] can only contain letters, numbers and underscores.
|
|
|
|
path : mandatory, the local path to your git repository
|
|
name : optional (default : [repoCode]), a cleaner name that will be displayed
|
|
user : optional (default : empty), the owner of the repository
|
|
if set and different from the user running the plugin, the git commands
|
|
will be executed as this user
|
|
warning : optional (default 10), the warning threshold
|
|
critical : optional (default 100), the critical threshold
|
|
|
|
For example :
|
|
|
|
[git_commit_behind]
|
|
user root
|
|
|
|
env.repo.munin_contrib.path /opt/munin-contrib
|
|
env.repo.munin_contrib.name Munin Contrib
|
|
|
|
env.repo.other_repo.path /path/to/other-repo
|
|
env.repo.other_repo.name Other Repo
|
|
|
|
=head1 MAGIC MARKERS
|
|
|
|
#%# family=auto
|
|
#%# capabilities=autoconf
|
|
|
|
=head1 VERSION
|
|
|
|
1.0.0
|
|
|
|
=head1 AUTHOR
|
|
|
|
Neraud (https://github.com/Neraud)
|
|
|
|
=head1 LICENSE
|
|
|
|
GPLv2
|
|
|
|
=cut"""
|
|
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
from random import randint
|
|
import re
|
|
from shlex import quote
|
|
from subprocess import check_output, call, DEVNULL, CalledProcessError
|
|
import sys
|
|
import time
|
|
|
|
|
|
plugin_version = "1.0.0"
|
|
|
|
if int(os.getenv('MUNIN_DEBUG', 0)) > 0:
|
|
logging.basicConfig(level=logging.DEBUG,
|
|
format='%(asctime)s %(levelname)-7s %(message)s')
|
|
|
|
current_user = pwd.getpwuid(os.geteuid())[0]
|
|
|
|
conf = {
|
|
'git_path': os.getenv('git_path', '/usr/bin/git'),
|
|
'state_file': os.getenv('MUNIN_STATEFILE'),
|
|
'update_mode': os.getenv('update.mode', 'munin'),
|
|
'update_probability': int(os.getenv('update.probability', '12')),
|
|
'update_maxinterval': int(os.getenv('update.maxinterval', '7200'))
|
|
}
|
|
|
|
repo_codes = set(re.search('repo\.([^.]+)\..*', elem).group(1)
|
|
for elem in os.environ.keys() if elem.startswith('repo.'))
|
|
|
|
repos_conf = {}
|
|
for code in repo_codes:
|
|
repos_conf[code] = {
|
|
'name': os.getenv('repo.%s.name' % code, code),
|
|
'path': os.getenv('repo.%s.path' % code, None),
|
|
'user': os.getenv('repo.%s.user' % code, None),
|
|
'warning': os.getenv('repo.%s.warning' % code, '10'),
|
|
'critical': os.getenv('repo.%s.critical' % code, '100')
|
|
}
|
|
|
|
|
|
def print_config():
|
|
print('graph_title Git repositories - Commits behind')
|
|
|
|
print('graph_args --base 1000 -r --lower-limit 0')
|
|
print('graph_vlabel number of commits behind')
|
|
print('graph_scale yes')
|
|
print('graph_info This graph shows the number of commits behind' +
|
|
' for each configured git repository')
|
|
print('graph_category file_transfer')
|
|
|
|
print('graph_order %s' % ' '.join(repo_codes))
|
|
|
|
for repo_code in repos_conf.keys():
|
|
print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
|
|
print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
|
|
print('%s.critical %s' %
|
|
(repo_code, repos_conf[repo_code]['critical']))
|
|
|
|
|
|
def generate_git_command(repo_conf, git_command):
|
|
if not repo_conf['user'] or repo_conf['user'] == current_user:
|
|
cmd = [quote(conf['git_path'])] + git_command
|
|
else:
|
|
shell_cmd = 'cd %s ; %s %s' % (
|
|
quote(repo_conf['path']),
|
|
quote(conf['git_path']),
|
|
' '.join(git_command))
|
|
cmd = ['su', '-', repo_conf['user'], '-s', '/bin/sh', '-c', shell_cmd]
|
|
return cmd
|
|
|
|
|
|
def execute_git_command(repo_conf, git_command):
|
|
cmd = generate_git_command(repo_conf, git_command)
|
|
return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
|
|
|
|
|
|
def print_info():
|
|
if not os.access(conf['git_path'], os.X_OK):
|
|
print('Git (%s) is missing, or not executable !' %
|
|
conf['git_path'], file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
for repo_code in repos_conf.keys():
|
|
logging.debug(' - %s' % repo_code)
|
|
try:
|
|
remote_branch = execute_git_command(
|
|
repos_conf[repo_code],
|
|
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
|
logging.debug('remote_branch = %s' % remote_branch)
|
|
|
|
commits_behind = execute_git_command(
|
|
repos_conf[repo_code],
|
|
['rev-list', 'HEAD..%s' % remote_branch, '--count'])
|
|
|
|
print('%s.value %d' % (repo_code, int(commits_behind)))
|
|
except CalledProcessError as e:
|
|
logging.error('Error executing git command : %s', e)
|
|
except FileNotFoundError as e:
|
|
logging.error('Repo not found at path %s' %
|
|
repos_conf[repo_code]['path'])
|
|
|
|
|
|
def check_update_repos(mode):
|
|
if not conf['state_file']:
|
|
logging.error('Munin state file unavailable')
|
|
sys.exit(1)
|
|
|
|
if mode != conf['update_mode']:
|
|
logging.debug('Wrong mode, skipping')
|
|
return
|
|
|
|
if not os.path.isfile(conf['state_file']):
|
|
logging.debug('No state file -> updating')
|
|
do_update_repos()
|
|
elif (os.path.getmtime(conf['state_file']) + conf['update_maxinterval']
|
|
< time.time()):
|
|
logging.debug('State file last modified too long ago -> updating')
|
|
do_update_repos()
|
|
elif randint(1, conf['update_probability']) == 1:
|
|
logging.debug('Recent state, but random matched -> updating')
|
|
do_update_repos()
|
|
else:
|
|
logging.debug('Recent state and random missed -> skipping')
|
|
|
|
|
|
def do_update_repos():
|
|
for repo_code in repos_conf.keys():
|
|
try:
|
|
logging.info('Fetching repo %s' % repo_code)
|
|
execute_git_command(repos_conf[repo_code], ['fetch'])
|
|
except CalledProcessError as e:
|
|
logging.error('Error executing git command : %s', e)
|
|
except FileNotFoundError as e:
|
|
logging.error('Repo not found at path %s' %
|
|
repos_conf[repo_code]['path'])
|
|
logging.debug('Updating the state file')
|
|
|
|
# 'touch' the state file to update its last modified date
|
|
Path(conf['state_file']).touch()
|
|
|
|
|
|
if len(sys.argv) > 1:
|
|
action = sys.argv[1]
|
|
if action == 'config':
|
|
print_config()
|
|
elif action == 'autoconf':
|
|
errors = []
|
|
|
|
if not conf['state_file']:
|
|
errors.append('munin state file unavailable')
|
|
|
|
if os.access(conf['git_path'], os.X_OK):
|
|
test_git = call([conf['git_path'], '--version'], stdout=DEVNULL)
|
|
if test_git != 0:
|
|
errors.append('git seems to be broken ?!')
|
|
else:
|
|
errors.append('git is missing or not executable')
|
|
|
|
if errors:
|
|
print('no (%s)' % ', '.join(errors))
|
|
else:
|
|
print('yes')
|
|
elif action == 'version':
|
|
print('Git commit behind Munin plugin, version {0}'.format(
|
|
plugin_version))
|
|
elif action == 'update':
|
|
check_update_repos('cron')
|
|
else:
|
|
logging.warn("Unknown argument '%s'" % action)
|
|
sys.exit(1)
|
|
else:
|
|
if conf['update_mode'] == 'munin':
|
|
check_update_repos('munin')
|
|
print_info()
|