root / plugins / git / git_commit_behind @ e29c89c0
Historique | Voir | Annoter | Télécharger (9,34 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 (for each munin period) and thus |
| 15 |
slowing down the data collection, the git fetch operation is only randomly |
| 16 |
triggered (based on env.update.probability). |
| 17 |
In case of very time-consuming update operations, you can run them in a |
| 18 |
separate cron job. |
| 19 |
|
| 20 |
=head1 REQUIREMENTS |
| 21 |
|
| 22 |
- Python3 |
| 23 |
- Git |
| 24 |
|
| 25 |
=head1 INSTALLATION |
| 26 |
|
| 27 |
Link this plugin, as usual. |
| 28 |
For example : |
| 29 |
ln -s /path/to/git_commit_behind /etc/munin/plugins/git_commit_behind |
| 30 |
|
| 31 |
If you wish to update the repositories via cron and not during the plugin |
| 32 |
execution (cf CONFIGURATION section), you need a dedicated cron job. |
| 33 |
|
| 34 |
For example, you can use the following cron : |
| 35 |
|
| 36 |
# If the git_commit_behind plugin is enabled, fetch git repositories randomly |
| 37 |
# according to the plugin configuration. |
| 38 |
# By default : once an hour (12 invocations an hour, 1 in 12 chance that the |
| 39 |
# update will 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 >/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 [user] |
| 48 |
env.git_path /path/to/git |
| 49 |
env.update.mode [munin|cron] |
| 50 |
env.update.probability 12 |
| 51 |
env.update.maxinterval 7200 |
| 52 |
|
| 53 |
user [user] : required, the owner of the repository checkouts |
| 54 |
in case of multiple different owners, use root |
| 55 |
env.git_path : optional (default : /usr/bin/git), the path to the git binary. |
| 56 |
env.update.mode : optional (default : munin), the update mode. |
| 57 |
munin : repositories are git fetched during the pugin execution |
| 58 |
cron : a dedicated cron job needs to be used to update the repositories |
| 59 |
env.update.probability : optional (default : 12), |
| 60 |
runs the update randomly (1 in <probability> chances) |
| 61 |
env.update.maxinterval : optional (default : 7200), |
| 62 |
ensures that the update is run at least every <maxinterval> seconds |
| 63 |
|
| 64 |
|
| 65 |
Then, for each repository you want to check, you need the following |
| 66 |
configuration block under the git_commit_behind section |
| 67 |
env.repo.[repoCode].path /path/to/local/repo |
| 68 |
env.repo.[repoCode].name Repo Name |
| 69 |
env.repo.[repoCode].user user |
| 70 |
env.repo.[repoCode].warning 10 |
| 71 |
env.repo.[repoCode].critical 100 |
| 72 |
|
| 73 |
[repoCode] can only contain letters, numbers and underscores. |
| 74 |
|
| 75 |
path : mandatory, the local path to your git repository |
| 76 |
name : optional (default : [repoCode]), a cleaner name that will be displayed |
| 77 |
user : optional (default : empty), the owner of the repository |
| 78 |
if set and different from the user running the plugin, the git commands |
| 79 |
will be executed as this user |
| 80 |
warning : optional (default 10), the warning threshold |
| 81 |
critical : optional (default 100), the critical threshold |
| 82 |
|
| 83 |
For example : |
| 84 |
|
| 85 |
[git_commit_behind] |
| 86 |
user root |
| 87 |
|
| 88 |
env.repo.munin_contrib.path /opt/munin-contrib |
| 89 |
env.repo.munin_contrib.name Munin Contrib |
| 90 |
|
| 91 |
env.repo.other_repo.path /path/to/other-repo |
| 92 |
env.repo.other_repo.name Other Repo |
| 93 |
|
| 94 |
=head1 MAGIC MARKERS |
| 95 |
|
| 96 |
#%# family=auto |
| 97 |
#%# capabilities=autoconf |
| 98 |
|
| 99 |
=head1 VERSION |
| 100 |
|
| 101 |
1.0.0 |
| 102 |
|
| 103 |
=head1 AUTHOR |
| 104 |
|
| 105 |
Neraud (https://github.com/Neraud) |
| 106 |
|
| 107 |
=head1 LICENSE |
| 108 |
|
| 109 |
GPLv2 |
| 110 |
|
| 111 |
=cut""" |
| 112 |
|
| 113 |
|
| 114 |
import logging |
| 115 |
import os |
| 116 |
from pathlib import Path |
| 117 |
from random import randint |
| 118 |
import re |
| 119 |
from shlex import quote |
| 120 |
from subprocess import check_output, call, DEVNULL, CalledProcessError |
| 121 |
import sys |
| 122 |
import time |
| 123 |
|
| 124 |
|
| 125 |
plugin_version = "1.0.0" |
| 126 |
|
| 127 |
if int(os.getenv('MUNIN_DEBUG', 0)) > 0:
|
| 128 |
logging.basicConfig(level=logging.DEBUG, |
| 129 |
format='%(asctime)s %(levelname)-7s %(message)s') |
| 130 |
|
| 131 |
conf = {
|
| 132 |
'git_path': os.getenv('git_path', '/usr/bin/git'),
|
| 133 |
'state_file': os.getenv('MUNIN_STATEFILE'),
|
| 134 |
'update_mode': os.getenv('update.mode', 'munin'),
|
| 135 |
'update_probability': int(os.getenv('update.probability', '12')),
|
| 136 |
'update_maxinterval': int(os.getenv('update.maxinterval', '7200'))
|
| 137 |
} |
| 138 |
|
| 139 |
repo_codes = set(re.search('repo\.([^.]+)\..*', elem).group(1)
|
| 140 |
for elem in os.environ.keys() if elem.startswith('repo.'))
|
| 141 |
|
| 142 |
repos_conf = {}
|
| 143 |
for code in repo_codes: |
| 144 |
repos_conf[code] = {
|
| 145 |
'name': os.getenv('repo.%s.name' % code, code),
|
| 146 |
'path': os.getenv('repo.%s.path' % code, None),
|
| 147 |
'user': os.getenv('repo.%s.user' % code, None),
|
| 148 |
'warning': os.getenv('repo.%s.warning' % code, '10'),
|
| 149 |
'critical': os.getenv('repo.%s.critical' % code, '100')
|
| 150 |
} |
| 151 |
|
| 152 |
|
| 153 |
def print_config(): |
| 154 |
print('graph_title Git repositories - Commits behind')
|
| 155 |
|
| 156 |
print('graph_args --base 1000 -r --lower-limit 0')
|
| 157 |
print('graph_vlabel number of commits behind')
|
| 158 |
print('graph_scale yes')
|
| 159 |
print('graph_info This graph shows the number of commits behind' +
|
| 160 |
' for each configured git repository') |
| 161 |
print('graph_category file_transfer')
|
| 162 |
|
| 163 |
print('graph_order %s' % ' '.join(repo_codes))
|
| 164 |
|
| 165 |
for repo_code in repos_conf.keys(): |
| 166 |
print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
|
| 167 |
print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
|
| 168 |
print('%s.critical %s' %
|
| 169 |
(repo_code, repos_conf[repo_code]['critical'])) |
| 170 |
|
| 171 |
|
| 172 |
def generate_git_command(repo_conf, git_command): |
| 173 |
if not repo_conf['user'] or repo_conf['user'] == os.environ['USER']: |
| 174 |
cmd = [quote(conf['git_path'])] + git_command |
| 175 |
else: |
| 176 |
shell_cmd = 'cd %s ; %s %s' % ( |
| 177 |
quote(repo_conf['path']), |
| 178 |
quote(conf['git_path']), |
| 179 |
' '.join(git_command)) |
| 180 |
cmd = ['su', '-', repo_conf['user'], '-c', shell_cmd] |
| 181 |
return cmd |
| 182 |
|
| 183 |
|
| 184 |
def execute_git_command(repo_conf, git_command): |
| 185 |
cmd = generate_git_command(repo_conf, git_command) |
| 186 |
return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
|
| 187 |
|
| 188 |
|
| 189 |
def print_info(): |
| 190 |
if not os.access(conf['git_path'], os.X_OK): |
| 191 |
print('Git (%s) is missing, or not executable !' %
|
| 192 |
conf['git_path'], file=sys.stderr) |
| 193 |
sys.exit(1) |
| 194 |
|
| 195 |
for repo_code in repos_conf.keys(): |
| 196 |
logging.debug(' - %s' % repo_code)
|
| 197 |
try: |
| 198 |
remote_branch = execute_git_command( |
| 199 |
repos_conf[repo_code], |
| 200 |
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
| 201 |
logging.debug('remote_branch = %s' % remote_branch)
|
| 202 |
|
| 203 |
commits_behind = execute_git_command( |
| 204 |
repos_conf[repo_code], |
| 205 |
['rev-list', 'HEAD..%s' % remote_branch, '--count']) |
| 206 |
|
| 207 |
print('%s.value %d' % (repo_code, int(commits_behind)))
|
| 208 |
except CalledProcessError as e: |
| 209 |
logging.error('Error executing git command : %s', e)
|
| 210 |
except FileNotFoundError as e: |
| 211 |
logging.error('Repo not found at path %s' %
|
| 212 |
repos_conf[repo_code]['path']) |
| 213 |
|
| 214 |
|
| 215 |
def check_update_repos(mode): |
| 216 |
if not conf['state_file']: |
| 217 |
logging.error('Munin state file unavailable')
|
| 218 |
sys.exit(1) |
| 219 |
|
| 220 |
if mode != conf['update_mode']: |
| 221 |
logging.debug('Wrong mode, skipping')
|
| 222 |
return |
| 223 |
|
| 224 |
if not os.path.isfile(conf['state_file']): |
| 225 |
logging.debug('No state file -> updating')
|
| 226 |
do_update_repos() |
| 227 |
elif (os.path.getmtime(conf['state_file']) + conf['update_maxinterval'] |
| 228 |
< time.time()): |
| 229 |
logging.debug('State file last modified too long ago -> updating')
|
| 230 |
do_update_repos() |
| 231 |
elif randint(1, conf['update_probability']) == 1: |
| 232 |
logging.debug('Recent state, but random matched -> updating')
|
| 233 |
do_update_repos() |
| 234 |
else: |
| 235 |
logging.debug('Recent state and random missed -> skipping')
|
| 236 |
|
| 237 |
|
| 238 |
def do_update_repos(): |
| 239 |
for repo_code in repos_conf.keys(): |
| 240 |
try: |
| 241 |
logging.info('Fetching repo %s' % repo_code)
|
| 242 |
execute_git_command(repos_conf[repo_code], ['fetch']) |
| 243 |
except CalledProcessError as e: |
| 244 |
logging.error('Error executing git command : %s', e)
|
| 245 |
except FileNotFoundError as e: |
| 246 |
logging.error('Repo not found at path %s' %
|
| 247 |
repos_conf[repo_code]['path']) |
| 248 |
logging.debug('Updating the state file')
|
| 249 |
|
| 250 |
# 'touch' the state file to update its last modified date |
| 251 |
Path(conf['state_file']).touch() |
| 252 |
|
| 253 |
|
| 254 |
if len(sys.argv) > 1: |
| 255 |
action = sys.argv[1] |
| 256 |
if action == 'config': |
| 257 |
print_config() |
| 258 |
elif action == 'autoconf': |
| 259 |
errors = [] |
| 260 |
|
| 261 |
if not conf['state_file']: |
| 262 |
errors.append('munin state file unavailable')
|
| 263 |
|
| 264 |
if os.access(conf['git_path'], os.X_OK): |
| 265 |
test_git = call([conf['git_path'], '--version'], stdout=DEVNULL) |
| 266 |
if test_git != 0: |
| 267 |
errors.append('git seems to be broken ?!')
|
| 268 |
else: |
| 269 |
errors.append('git is missing or not executable')
|
| 270 |
|
| 271 |
if errors: |
| 272 |
print('no (%s)' % ', '.join(errors))
|
| 273 |
else: |
| 274 |
print('yes')
|
| 275 |
elif action == 'version': |
| 276 |
print('Git commit behind Munin plugin, version {0}'.format(
|
| 277 |
plugin_version)) |
| 278 |
elif action == 'update': |
| 279 |
check_update_repos('cron')
|
| 280 |
else: |
| 281 |
logging.warn("Unknown argument '%s'" % action)
|
| 282 |
sys.exit(1) |
| 283 |
else: |
| 284 |
if conf['update_mode'] == 'munin': |
| 285 |
check_update_repos('munin')
|
| 286 |
print_info() |
