root / plugins / git / git_commit_behind @ e0b243ba
Historique | Voir | Annoter | Télécharger (8,33 ko)
| 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 |
user root : required (to be able to switch to each repo user) |
| 51 |
env.git_path : optional (default : /usr/bin/git), the path to the git binary. |
| 52 |
|
| 53 |
Then, for each repository you want to check, you need the following |
| 54 |
configuration block under the git_commit_behind section |
| 55 |
env.repo.[repoCode].path /path/to/local/repo |
| 56 |
env.repo.[repoCode].name Repo Name |
| 57 |
env.repo.[repoCode].user user |
| 58 |
env.repo.[repoCode].warning 10 |
| 59 |
env.repo.[repoCode].critical 100 |
| 60 |
|
| 61 |
[repoCode] can only contain letters, numbers and underscores. |
| 62 |
|
| 63 |
path : mandatory, the local path to your git repository |
| 64 |
name : optional (default : [repoCode]), a cleaner name that will be displayed |
| 65 |
user : optional (default : empty), the owner of the repository |
| 66 |
if set and different from the user running the plugin, the git commands |
| 67 |
will be executed as this user |
| 68 |
warning : optional (default 10), the warning threshold |
| 69 |
critical : optional (default 100), the critical threshold |
| 70 |
|
| 71 |
For example : |
| 72 |
|
| 73 |
[git_commit_behind] |
| 74 |
user root |
| 75 |
|
| 76 |
env.repo.munin_contrib.path /opt/munin-contrib |
| 77 |
env.repo.munin_contrib.name Munin Contrib |
| 78 |
|
| 79 |
env.repo.other_repo.path /path/to/other-repo |
| 80 |
env.repo.other_repo.name Other Repo |
| 81 |
|
| 82 |
=head1 MAGIC MARKERS |
| 83 |
|
| 84 |
#%# family=auto |
| 85 |
#%# capabilities=autoconf |
| 86 |
|
| 87 |
=head1 VERSION |
| 88 |
|
| 89 |
1.0.0 |
| 90 |
|
| 91 |
=head1 AUTHOR |
| 92 |
|
| 93 |
Neraud (https://github.com/Neraud) |
| 94 |
|
| 95 |
=head1 LICENSE |
| 96 |
|
| 97 |
GPLv2 |
| 98 |
|
| 99 |
=cut""" |
| 100 |
|
| 101 |
|
| 102 |
import logging |
| 103 |
import os |
| 104 |
from pathlib import Path |
| 105 |
from random import randint |
| 106 |
import re |
| 107 |
from subprocess import check_output, call, DEVNULL, CalledProcessError |
| 108 |
import sys |
| 109 |
import time |
| 110 |
|
| 111 |
|
| 112 |
plugin_version = "1.0.0" |
| 113 |
|
| 114 |
debug = int(os.getenv('MUNIN_DEBUG', os.getenv('DEBUG', 0))) > 0
|
| 115 |
if debug: |
| 116 |
logging.basicConfig(level=logging.DEBUG, |
| 117 |
format='%(asctime)s %(levelname)-7s %(message)s') |
| 118 |
|
| 119 |
conf = {
|
| 120 |
'git_path': os.getenv('git_path', '/usr/bin/git'),
|
| 121 |
'state_file': os.getenv('MUNIN_STATEFILE',
|
| 122 |
'/var/lib/munin-node/plugin-state/nobody/' + |
| 123 |
'git_commit_behind.state') |
| 124 |
} |
| 125 |
|
| 126 |
repo_codes = set(re.search('repo\.([^.]+)\..*', elem).group(1)
|
| 127 |
for elem in os.environ.keys() if elem.startswith('repo.'))
|
| 128 |
|
| 129 |
repos_conf = {}
|
| 130 |
for code in repo_codes: |
| 131 |
repos_conf[code] = {
|
| 132 |
'name': os.getenv('repo.%s.name' % code, code),
|
| 133 |
'path': os.getenv('repo.%s.path' % code, None),
|
| 134 |
'user': os.getenv('repo.%s.user' % code, None),
|
| 135 |
'warning': os.getenv('repo.%s.warning' % code, '10'),
|
| 136 |
'critical': os.getenv('repo.%s.critical' % code, '100')
|
| 137 |
} |
| 138 |
|
| 139 |
|
| 140 |
def print_config(): |
| 141 |
print('graph_title Git repositories - Commits behind')
|
| 142 |
|
| 143 |
print('graph_args --base 1000 -r --lower-limit 0')
|
| 144 |
print('graph_vlabel number of commits behind')
|
| 145 |
print('graph_scale yes')
|
| 146 |
print('graph_info This graph shows the number of commits behind' +
|
| 147 |
' for each configured git repository') |
| 148 |
print('graph_category system')
|
| 149 |
|
| 150 |
print('graph_order %s' % ' '.join(repo_codes))
|
| 151 |
|
| 152 |
for repo_code in repos_conf.keys(): |
| 153 |
print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
|
| 154 |
print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
|
| 155 |
print('%s.critical %s' %
|
| 156 |
(repo_code, repos_conf[repo_code]['critical'])) |
| 157 |
|
| 158 |
|
| 159 |
def generate_git_command(repo_conf, git_command): |
| 160 |
if not repo_conf['user'] or repo_conf['user'] == os.environ['USER']: |
| 161 |
cmd = [conf['git_path']] + git_command |
| 162 |
else: |
| 163 |
shell_cmd = 'cd %s ; %s %s' % ( |
| 164 |
repo_conf['path'], conf['git_path'], ' '.join(git_command)) |
| 165 |
cmd = ['su', '-', repo_conf['user'], '-c', shell_cmd] |
| 166 |
return cmd |
| 167 |
|
| 168 |
|
| 169 |
def execute_git_command(repo_conf, git_command): |
| 170 |
cmd = generate_git_command(repo_conf, git_command) |
| 171 |
return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
|
| 172 |
|
| 173 |
|
| 174 |
def get_info(): |
| 175 |
if not os.access(conf['git_path'], os.X_OK): |
| 176 |
print('Git (%s) is missing, or not executable !' %
|
| 177 |
conf['git_path'], file=sys.stderr) |
| 178 |
sys.exit(1) |
| 179 |
|
| 180 |
for repo_code in repos_conf.keys(): |
| 181 |
logging.debug(' - %s' % repo_code)
|
| 182 |
try: |
| 183 |
remote_branch = execute_git_command( |
| 184 |
repos_conf[repo_code], |
| 185 |
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
| 186 |
logging.debug('remote_branch = %s' % remote_branch)
|
| 187 |
|
| 188 |
commits_behind = execute_git_command( |
| 189 |
repos_conf[repo_code], |
| 190 |
['rev-list', 'HEAD..%s' % remote_branch, '--count']) |
| 191 |
|
| 192 |
print('%s.value %d' % (repo_code, int(commits_behind)))
|
| 193 |
except CalledProcessError as e: |
| 194 |
logging.error('Error executing git command : %s', e)
|
| 195 |
except FileNotFoundError as e: |
| 196 |
logging.error('Repo not found at path %s' %
|
| 197 |
repos_conf[repo_code]['path']) |
| 198 |
|
| 199 |
|
| 200 |
def check_update_repos(): |
| 201 |
if len(sys.argv) > 2: |
| 202 |
max_interval = int(sys.argv[2]) |
| 203 |
else: |
| 204 |
max_interval = 7200 |
| 205 |
|
| 206 |
if len(sys.argv) > 3: |
| 207 |
probability = int(sys.argv[3]) |
| 208 |
else: |
| 209 |
probability = 12 |
| 210 |
|
| 211 |
if not os.path.isfile(conf['state_file']): |
| 212 |
logging.debug('No state file -> updating')
|
| 213 |
do_update_repos() |
| 214 |
elif os.path.getmtime(conf['state_file']) + max_interval < time.time(): |
| 215 |
logging.debug('State file last modified too long ago -> updating')
|
| 216 |
do_update_repos() |
| 217 |
elif randint(1, probability) == 1: |
| 218 |
logging.debug('Recent state, but random matched -> updating')
|
| 219 |
do_update_repos() |
| 220 |
else: |
| 221 |
logging.debug('Recent state and random missed -> skipping')
|
| 222 |
|
| 223 |
|
| 224 |
def do_update_repos(): |
| 225 |
for repo_code in repos_conf.keys(): |
| 226 |
try: |
| 227 |
logging.info('Fetching repo %s' % repo_code)
|
| 228 |
execute_git_command(repos_conf[repo_code], ['fetch']) |
| 229 |
except CalledProcessError as e: |
| 230 |
logging.error('Error executing git command : %s', e)
|
| 231 |
except FileNotFoundError as e: |
| 232 |
logging.error('Repo not found at path %s' %
|
| 233 |
repos_conf[repo_code]['path']) |
| 234 |
logging.debug('Updating the state file')
|
| 235 |
|
| 236 |
# 'touch' the state file to update its last modified date |
| 237 |
Path(conf['state_file']).touch() |
| 238 |
|
| 239 |
|
| 240 |
if len(sys.argv) > 1: |
| 241 |
action = sys.argv[1] |
| 242 |
if action == 'config': |
| 243 |
print_config() |
| 244 |
elif action == 'autoconf': |
| 245 |
if os.access(conf['git_path'], os.X_OK): |
| 246 |
test_git = call([conf['git_path'], '--version'], stdout=DEVNULL) |
| 247 |
if test_git == 0: |
| 248 |
print('yes')
|
| 249 |
else: |
| 250 |
print('no (git seems to be broken ?!)')
|
| 251 |
else: |
| 252 |
print('no (git is missing or not executable)')
|
| 253 |
elif action == 'version': |
| 254 |
print('Git commit behind Munin plugin, version {0}'.format(
|
| 255 |
plugin_version)) |
| 256 |
elif action == 'update': |
| 257 |
check_update_repos() |
| 258 |
elif action: |
| 259 |
logging.warn("Unknown argument '%s'" % action)
|
| 260 |
sys.exit(1) |
| 261 |
else: |
| 262 |
get_info() |
| 263 |
else: |
| 264 |
get_info() |
