Révision a26b9e8d
Added git_commit_behind
| plugins/git/git_commit_behind | ||
|---|---|---|
| 1 |
#! /usr/bin/env python3 |
|
| 2 |
|
|
| 3 |
"""=cut |
|
| 4 |
=head1 NAME |
|
| 5 |
|
|
| 6 |
git_commit_behind - Munin plugin to monitor local git repositories and report |
|
| 7 |
how many commits behind their remote they are |
|
| 8 |
|
|
| 9 |
=head1 NOTES |
|
| 10 |
|
|
| 11 |
This plugin is similar to how apt_all works for apt packages. |
|
| 12 |
|
|
| 13 |
To be able to check how behind a git repository is, we need to run git fetch. |
|
| 14 |
To avoid fetching all repos every 5 minutes and slowing down the munin-node, |
|
| 15 |
the fetch operation is triggered via a cron job. |
|
| 16 |
|
|
| 17 |
=head1 REQUIREMENTS |
|
| 18 |
|
|
| 19 |
- Python3 |
|
| 20 |
- Git |
|
| 21 |
|
|
| 22 |
=head1 INSTALLATION |
|
| 23 |
|
|
| 24 |
Link this plugin, as usual. |
|
| 25 |
For example : |
|
| 26 |
ln -s /path/to/git_commit_behind /etc/munin/plugins/git_commit_behind |
|
| 27 |
|
|
| 28 |
You also need to setup a cron job to trigger the git fetches. |
|
| 29 |
|
|
| 30 |
The plugin can be called with an "update" mode to handle the fetches : |
|
| 31 |
munin-run git_commit_behind update <maxinterval> <probability> |
|
| 32 |
It will run the fetches randomly (1 in <probability> chances), |
|
| 33 |
and ensure that it is run at least every <maxinterval> seconds. |
|
| 34 |
|
|
| 35 |
For example, you can use the following cron : |
|
| 36 |
|
|
| 37 |
# If the git_commit_behind plugin is enabled, fetch git repositories approx. |
|
| 38 |
# once an hour (12 invocations an hour, 1 in 12 chance that the update will |
|
| 39 |
# happen), but ensure that there will never be more than two hours |
|
| 40 |
# (7200 seconds) interval between updates. |
|
| 41 |
*/5 * * * * root if [ -x /etc/munin/plugins/git_commit_behind ]; then /usr/sbin/munin-run git_commit_behind update 7200 12 >/dev/null; fi |
|
| 42 |
|
|
| 43 |
=head1 CONFIGURATION |
|
| 44 |
|
|
| 45 |
Use your "/etc/munin/plugin-conf.d/munin-node" to configure this plugin. |
|
| 46 |
[git_commit_behind] |
|
| 47 |
user root |
|
| 48 |
env.git_path /path/to/git |
|
| 49 |
|
|
| 50 |
Then, for each repository you want to check, you need the following |
|
| 51 |
configuration block under the git_commit_behind section |
|
| 52 |
env.repo.[repoCode].path /path/to/local/repo |
|
| 53 |
env.repo.[repoCode].name Repo Name |
|
| 54 |
env.repo.[repoCode].user user |
|
| 55 |
env.repo.[repoCode].warning 10 |
|
| 56 |
env.repo.[repoCode].critical 100 |
|
| 57 |
|
|
| 58 |
[repoCode] can only contain letters, numbers and underscores. |
|
| 59 |
|
|
| 60 |
path : mandatory, the local path to your git repository |
|
| 61 |
name : optional (default : [repoCode]), a cleaner name that will be displayed |
|
| 62 |
user : optional (default : empty), the owner of the repository |
|
| 63 |
if set and different from the user running the plugin, the git commands |
|
| 64 |
will be executed as this user |
|
| 65 |
warning : optional (default 10), the warning threshold |
|
| 66 |
critical : optional (default 100), the critical threshold |
|
| 67 |
|
|
| 68 |
For example : |
|
| 69 |
|
|
| 70 |
[git_commit_behind] |
|
| 71 |
user root |
|
| 72 |
|
|
| 73 |
env.repo.munin_contrib.path /opt/munin-contrib |
|
| 74 |
env.repo.munin_contrib.name Munin Contrib |
|
| 75 |
|
|
| 76 |
env.repo.other_repo.path /path/to/other-repo |
|
| 77 |
env.repo.other_repo.name Other Repo |
|
| 78 |
|
|
| 79 |
=head1 MAGIC MARKERS |
|
| 80 |
|
|
| 81 |
#%# family=auto |
|
| 82 |
#%# capabilities=autoconf |
|
| 83 |
|
|
| 84 |
=head1 VERSION |
|
| 85 |
|
|
| 86 |
1.0.0 |
|
| 87 |
|
|
| 88 |
=head1 AUTHOR |
|
| 89 |
|
|
| 90 |
Neraud (https://github.com/Neraud) |
|
| 91 |
|
|
| 92 |
=head1 LICENSE |
|
| 93 |
|
|
| 94 |
GPLv2 |
|
| 95 |
|
|
| 96 |
=cut""" |
|
| 97 |
|
|
| 98 |
|
|
| 99 |
from __future__ import print_function |
|
| 100 |
|
|
| 101 |
import logging |
|
| 102 |
import os |
|
| 103 |
from random import randint |
|
| 104 |
import re |
|
| 105 |
from subprocess import check_output, call, DEVNULL, CalledProcessError |
|
| 106 |
import sys |
|
| 107 |
import time |
|
| 108 |
|
|
| 109 |
|
|
| 110 |
plugin_version = "1.0.0" |
|
| 111 |
|
|
| 112 |
debug = int(os.getenv('MUNIN_DEBUG', os.getenv('DEBUG', 0))) > 0
|
|
| 113 |
if debug: |
|
| 114 |
logging.basicConfig(level=logging.DEBUG, |
|
| 115 |
format='%(asctime)s %(levelname)-7s %(message)s') |
|
| 116 |
|
|
| 117 |
conf = {
|
|
| 118 |
'git_path': os.getenv('git_path', '/usr/bin/git'),
|
|
| 119 |
'state_file': os.getenv('MUNIN_STATEFILE',
|
|
| 120 |
'/var/lib/munin-node/plugin-state/nobody/' + |
|
| 121 |
'git_commit_behind.state') |
|
| 122 |
} |
|
| 123 |
|
|
| 124 |
repo_codes = set([re.search('repo\.([^.]+)\..*', elem).group(1)
|
|
| 125 |
for elem in os.environ.keys() if 'repo.' in elem]) |
|
| 126 |
|
|
| 127 |
repos_conf = {}
|
|
| 128 |
for code in repo_codes: |
|
| 129 |
repos_conf[code] = {
|
|
| 130 |
'name': os.getenv('repo.%s.name' % code, code),
|
|
| 131 |
'path': os.getenv('repo.%s.path' % code, None),
|
|
| 132 |
'user': os.getenv('repo.%s.user' % code, None),
|
|
| 133 |
'warning': os.getenv('repo.%s.warning' % code, '10'),
|
|
| 134 |
'critical': os.getenv('repo.%s.critical' % code, '100')
|
|
| 135 |
} |
|
| 136 |
|
|
| 137 |
|
|
| 138 |
def print_config(): |
|
| 139 |
print('graph_title Git repositories - Commits behind')
|
|
| 140 |
|
|
| 141 |
print('graph_args --base 1000 -r --lower-limit 0')
|
|
| 142 |
print('graph_vlabel number of commits behind')
|
|
| 143 |
print('graph_scale yes')
|
|
| 144 |
print('graph_info This graph shows the number of commits behind' +
|
|
| 145 |
' for each configured git repository') |
|
| 146 |
print('graph_category system')
|
|
| 147 |
|
|
| 148 |
print('graph_order %s' % ' '.join(repo_codes))
|
|
| 149 |
|
|
| 150 |
for repo_code in repos_conf.keys(): |
|
| 151 |
print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
|
|
| 152 |
print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
|
|
| 153 |
print('%s.critical %s' %
|
|
| 154 |
(repo_code, repos_conf[repo_code]['critical'])) |
|
| 155 |
|
|
| 156 |
|
|
| 157 |
def generate_git_command(repo_conf, git_command): |
|
| 158 |
if not repo_conf['user'] or repo_conf['user'] == os.environ['USER']: |
|
| 159 |
cmd = [conf['git_path']] + git_command |
|
| 160 |
else: |
|
| 161 |
shell_cmd = 'cd %s ; %s %s' % ( |
|
| 162 |
repo_conf['path'], conf['git_path'], ' '.join(git_command)) |
|
| 163 |
cmd = ['su', '-', repo_conf['user'], '-c', shell_cmd] |
|
| 164 |
return cmd |
|
| 165 |
|
|
| 166 |
|
|
| 167 |
def execute_git_command(repo_conf, git_command): |
|
| 168 |
cmd = generate_git_command(repo_conf, git_command) |
|
| 169 |
return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
|
|
| 170 |
|
|
| 171 |
|
|
| 172 |
def get_info(): |
|
| 173 |
if not os.access(conf['git_path'], os.X_OK): |
|
| 174 |
print('Git (%s) is missing, or not executable !' %
|
|
| 175 |
conf['git_path'], file=sys.stderr) |
|
| 176 |
sys.exit(1) |
|
| 177 |
|
|
| 178 |
for repo_code in repos_conf.keys(): |
|
| 179 |
logging.debug(' - %s' % repo_code)
|
|
| 180 |
try: |
|
| 181 |
remote_branch = execute_git_command( |
|
| 182 |
repos_conf[repo_code], |
|
| 183 |
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
|
| 184 |
logging.debug('remote_branch = %s' % remote_branch)
|
|
| 185 |
|
|
| 186 |
commits_behind = execute_git_command( |
|
| 187 |
repos_conf[repo_code], |
|
| 188 |
['rev-list', 'HEAD..%s' % remote_branch, '--count']) |
|
| 189 |
|
|
| 190 |
print('%s.value %d' % (repo_code, int(commits_behind)))
|
|
| 191 |
except CalledProcessError as e: |
|
| 192 |
logging.error('Error executing git command : %s' % str(e))
|
|
| 193 |
except FileNotFoundError as e: |
|
| 194 |
logging.error('Repo not found at path %s' %
|
|
| 195 |
repos_conf[repo_code]['path']) |
|
| 196 |
|
|
| 197 |
|
|
| 198 |
def check_update_repos(): |
|
| 199 |
if len(sys.argv) > 2: |
|
| 200 |
max_interval = int(sys.argv[2]) |
|
| 201 |
else: |
|
| 202 |
max_interval = 7200 |
|
| 203 |
|
|
| 204 |
if len(sys.argv) > 3: |
|
| 205 |
probability = int(sys.argv[3]) |
|
| 206 |
else: |
|
| 207 |
probability = 12 |
|
| 208 |
|
|
| 209 |
if not os.path.isfile(conf['state_file']): |
|
| 210 |
logging.debug('No state file -> updating')
|
|
| 211 |
do_update_repos() |
|
| 212 |
elif os.path.getmtime(conf['state_file']) + max_interval < time.time(): |
|
| 213 |
logging.debug('State file last modified too long ago -> updating')
|
|
| 214 |
do_update_repos() |
|
| 215 |
elif randint(1, probability) == 1: |
|
| 216 |
logging.debug('Recent state, but random matched -> updating')
|
|
| 217 |
do_update_repos() |
|
| 218 |
else: |
|
| 219 |
logging.debug('Recent state and random missed -> skipping')
|
|
| 220 |
|
|
| 221 |
|
|
| 222 |
def do_update_repos(): |
|
| 223 |
for repo_code in repos_conf.keys(): |
|
| 224 |
try: |
|
| 225 |
logging.info('Fetching repo %s' % repo_code)
|
|
| 226 |
execute_git_command(repos_conf[repo_code], ['fetch']) |
|
| 227 |
except CalledProcessError as e: |
|
| 228 |
logging.error('Error executing git command : %s' % str(e))
|
|
| 229 |
except FileNotFoundError as e: |
|
| 230 |
logging.error('Repo not found at path %s' %
|
|
| 231 |
repos_conf[repo_code]['path']) |
|
| 232 |
logging.debug('Updating the state file')
|
|
| 233 |
open(conf['state_file'], 'w') |
|
| 234 |
|
|
| 235 |
|
|
| 236 |
if len(sys.argv) > 1: |
|
| 237 |
action = sys.argv[1] |
|
| 238 |
if action == 'config': |
|
| 239 |
print_config() |
|
| 240 |
elif action == 'autoconf': |
|
| 241 |
if os.access(conf['git_path'], os.X_OK): |
|
| 242 |
test_git = call([conf['git_path'], '--version'], stdout=DEVNULL) |
|
| 243 |
if test_git == 0: |
|
| 244 |
print('yes')
|
|
| 245 |
else: |
|
| 246 |
print('no (git seems to be broken ?!)')
|
|
| 247 |
else: |
|
| 248 |
print('no (git is missing or not executable)')
|
|
| 249 |
elif action == 'version': |
|
| 250 |
print('Git commit behind Munin plugin, version {0}'.format(
|
|
| 251 |
plugin_version)) |
|
| 252 |
elif action == 'update': |
|
| 253 |
check_update_repos() |
|
| 254 |
elif action: |
|
| 255 |
logging.warn('Unknown argument \'%s\'' % action)
|
|
| 256 |
sys.exit(1) |
|
| 257 |
else: |
|
| 258 |
get_info() |
|
| 259 |
else: |
|
| 260 |
get_info() |
|
Formats disponibles : Unified diff