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