root / plugins / git / git_commit_behind @ 646a6c69
Historique | Voir | Annoter | Télécharger (9,4 ko)
| 1 | a26b9e8d | Neraud | #! /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 | 1027f5e5 | Neraud | 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 | a26b9e8d | Neraud | |
| 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 | d079f0fa | Neraud | 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 | a26b9e8d | Neraud | |
| 34 | For example, you can use the following cron : |
||
| 35 | |||
| 36 | d079f0fa | Neraud | # 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 | a26b9e8d | Neraud | # (7200 seconds) interval between updates. |
| 41 | d079f0fa | Neraud | */5 * * * * root if [ -x /etc/munin/plugins/git_commit_behind ]; then /usr/sbin/munin-run git_commit_behind update >/dev/null; fi |
| 42 | a26b9e8d | Neraud | |
| 43 | =head1 CONFIGURATION |
||
| 44 | |||
| 45 | Use your "/etc/munin/plugin-conf.d/munin-node" to configure this plugin. |
||
| 46 | [git_commit_behind] |
||
| 47 | 0d538451 | Neraud | user [user] |
| 48 | a26b9e8d | Neraud | env.git_path /path/to/git |
| 49 | d079f0fa | Neraud | env.update.mode [munin|cron] |
| 50 | env.update.probability 12 |
||
| 51 | env.update.maxinterval 7200 |
||
| 52 | a26b9e8d | Neraud | |
| 53 | 0d538451 | Neraud | user [user] : required, the owner of the repository checkouts |
| 54 | in case of multiple different owners, use root |
||
| 55 | 7bc4d133 | Neraud | env.git_path : optional (default : /usr/bin/git), the path to the git binary. |
| 56 | d079f0fa | Neraud | 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 | 7bc4d133 | Neraud | |
| 65 | a26b9e8d | Neraud | 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 | e0b243ba | Neraud | from pathlib import Path |
| 117 | 646a6c69 | Neraud | import pwd |
| 118 | a26b9e8d | Neraud | from random import randint |
| 119 | import re |
||
| 120 | c169373f | Neraud | from shlex import quote |
| 121 | a26b9e8d | Neraud | from subprocess import check_output, call, DEVNULL, CalledProcessError |
| 122 | import sys |
||
| 123 | import time |
||
| 124 | |||
| 125 | |||
| 126 | plugin_version = "1.0.0" |
||
| 127 | |||
| 128 | e29c89c0 | Neraud | if int(os.getenv('MUNIN_DEBUG', 0)) > 0:
|
| 129 | a26b9e8d | Neraud | logging.basicConfig(level=logging.DEBUG, |
| 130 | format='%(asctime)s %(levelname)-7s %(message)s') |
||
| 131 | |||
| 132 | 646a6c69 | Neraud | current_user = pwd.getpwuid(os.geteuid())[0] |
| 133 | |||
| 134 | a26b9e8d | Neraud | conf = {
|
| 135 | d079f0fa | Neraud | 'git_path': os.getenv('git_path', '/usr/bin/git'),
|
| 136 | 'state_file': os.getenv('MUNIN_STATEFILE'),
|
||
| 137 | 'update_mode': os.getenv('update.mode', 'munin'),
|
||
| 138 | 'update_probability': int(os.getenv('update.probability', '12')),
|
||
| 139 | 'update_maxinterval': int(os.getenv('update.maxinterval', '7200'))
|
||
| 140 | a26b9e8d | Neraud | } |
| 141 | |||
| 142 | 397052a8 | Neraud | repo_codes = set(re.search('repo\.([^.]+)\..*', elem).group(1)
|
| 143 | for elem in os.environ.keys() if elem.startswith('repo.'))
|
||
| 144 | a26b9e8d | Neraud | |
| 145 | repos_conf = {}
|
||
| 146 | for code in repo_codes: |
||
| 147 | repos_conf[code] = {
|
||
| 148 | 'name': os.getenv('repo.%s.name' % code, code),
|
||
| 149 | 'path': os.getenv('repo.%s.path' % code, None),
|
||
| 150 | 'user': os.getenv('repo.%s.user' % code, None),
|
||
| 151 | 'warning': os.getenv('repo.%s.warning' % code, '10'),
|
||
| 152 | 'critical': os.getenv('repo.%s.critical' % code, '100')
|
||
| 153 | } |
||
| 154 | |||
| 155 | |||
| 156 | def print_config(): |
||
| 157 | print('graph_title Git repositories - Commits behind')
|
||
| 158 | |||
| 159 | print('graph_args --base 1000 -r --lower-limit 0')
|
||
| 160 | print('graph_vlabel number of commits behind')
|
||
| 161 | print('graph_scale yes')
|
||
| 162 | print('graph_info This graph shows the number of commits behind' +
|
||
| 163 | ' for each configured git repository') |
||
| 164 | eb9681a7 | Neraud | print('graph_category file_transfer')
|
| 165 | a26b9e8d | Neraud | |
| 166 | print('graph_order %s' % ' '.join(repo_codes))
|
||
| 167 | |||
| 168 | for repo_code in repos_conf.keys(): |
||
| 169 | print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
|
||
| 170 | print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
|
||
| 171 | print('%s.critical %s' %
|
||
| 172 | (repo_code, repos_conf[repo_code]['critical'])) |
||
| 173 | |||
| 174 | |||
| 175 | def generate_git_command(repo_conf, git_command): |
||
| 176 | 646a6c69 | Neraud | if not repo_conf['user'] or repo_conf['user'] == current_user: |
| 177 | c169373f | Neraud | cmd = [quote(conf['git_path'])] + git_command |
| 178 | a26b9e8d | Neraud | else: |
| 179 | shell_cmd = 'cd %s ; %s %s' % ( |
||
| 180 | c169373f | Neraud | quote(repo_conf['path']), |
| 181 | quote(conf['git_path']), |
||
| 182 | ' '.join(git_command)) |
||
| 183 | d5caa85c | Neraud | cmd = ['su', '-', repo_conf['user'], '-s', '/bin/sh', '-c', shell_cmd] |
| 184 | a26b9e8d | Neraud | return cmd |
| 185 | |||
| 186 | |||
| 187 | def execute_git_command(repo_conf, git_command): |
||
| 188 | cmd = generate_git_command(repo_conf, git_command) |
||
| 189 | return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
|
||
| 190 | |||
| 191 | |||
| 192 | 81b129dd | Neraud | def print_info(): |
| 193 | a26b9e8d | Neraud | if not os.access(conf['git_path'], os.X_OK): |
| 194 | print('Git (%s) is missing, or not executable !' %
|
||
| 195 | conf['git_path'], file=sys.stderr) |
||
| 196 | sys.exit(1) |
||
| 197 | |||
| 198 | for repo_code in repos_conf.keys(): |
||
| 199 | logging.debug(' - %s' % repo_code)
|
||
| 200 | try: |
||
| 201 | remote_branch = execute_git_command( |
||
| 202 | repos_conf[repo_code], |
||
| 203 | ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
||
| 204 | logging.debug('remote_branch = %s' % remote_branch)
|
||
| 205 | |||
| 206 | commits_behind = execute_git_command( |
||
| 207 | repos_conf[repo_code], |
||
| 208 | ['rev-list', 'HEAD..%s' % remote_branch, '--count']) |
||
| 209 | |||
| 210 | print('%s.value %d' % (repo_code, int(commits_behind)))
|
||
| 211 | except CalledProcessError as e: |
||
| 212 | 397052a8 | Neraud | logging.error('Error executing git command : %s', e)
|
| 213 | a26b9e8d | Neraud | except FileNotFoundError as e: |
| 214 | logging.error('Repo not found at path %s' %
|
||
| 215 | repos_conf[repo_code]['path']) |
||
| 216 | |||
| 217 | |||
| 218 | d079f0fa | Neraud | def check_update_repos(mode): |
| 219 | 61241b80 | Neraud | if not conf['state_file']: |
| 220 | logging.error('Munin state file unavailable')
|
||
| 221 | sys.exit(1) |
||
| 222 | |||
| 223 | d079f0fa | Neraud | if mode != conf['update_mode']: |
| 224 | logging.debug('Wrong mode, skipping')
|
||
| 225 | return |
||
| 226 | a26b9e8d | Neraud | |
| 227 | if not os.path.isfile(conf['state_file']): |
||
| 228 | logging.debug('No state file -> updating')
|
||
| 229 | do_update_repos() |
||
| 230 | d079f0fa | Neraud | elif (os.path.getmtime(conf['state_file']) + conf['update_maxinterval'] |
| 231 | < time.time()): |
||
| 232 | a26b9e8d | Neraud | logging.debug('State file last modified too long ago -> updating')
|
| 233 | do_update_repos() |
||
| 234 | d079f0fa | Neraud | elif randint(1, conf['update_probability']) == 1: |
| 235 | a26b9e8d | Neraud | logging.debug('Recent state, but random matched -> updating')
|
| 236 | do_update_repos() |
||
| 237 | else: |
||
| 238 | logging.debug('Recent state and random missed -> skipping')
|
||
| 239 | |||
| 240 | |||
| 241 | def do_update_repos(): |
||
| 242 | for repo_code in repos_conf.keys(): |
||
| 243 | try: |
||
| 244 | logging.info('Fetching repo %s' % repo_code)
|
||
| 245 | execute_git_command(repos_conf[repo_code], ['fetch']) |
||
| 246 | except CalledProcessError as e: |
||
| 247 | 397052a8 | Neraud | logging.error('Error executing git command : %s', e)
|
| 248 | a26b9e8d | Neraud | except FileNotFoundError as e: |
| 249 | logging.error('Repo not found at path %s' %
|
||
| 250 | repos_conf[repo_code]['path']) |
||
| 251 | logging.debug('Updating the state file')
|
||
| 252 | e0b243ba | Neraud | |
| 253 | # 'touch' the state file to update its last modified date |
||
| 254 | Path(conf['state_file']).touch() |
||
| 255 | a26b9e8d | Neraud | |
| 256 | |||
| 257 | if len(sys.argv) > 1: |
||
| 258 | action = sys.argv[1] |
||
| 259 | if action == 'config': |
||
| 260 | print_config() |
||
| 261 | elif action == 'autoconf': |
||
| 262 | 61241b80 | Neraud | errors = [] |
| 263 | |||
| 264 | if not conf['state_file']: |
||
| 265 | errors.append('munin state file unavailable')
|
||
| 266 | |||
| 267 | a26b9e8d | Neraud | if os.access(conf['git_path'], os.X_OK): |
| 268 | test_git = call([conf['git_path'], '--version'], stdout=DEVNULL) |
||
| 269 | 61241b80 | Neraud | if test_git != 0: |
| 270 | errors.append('git seems to be broken ?!')
|
||
| 271 | else: |
||
| 272 | errors.append('git is missing or not executable')
|
||
| 273 | |||
| 274 | if errors: |
||
| 275 | print('no (%s)' % ', '.join(errors))
|
||
| 276 | a26b9e8d | Neraud | else: |
| 277 | 61241b80 | Neraud | print('yes')
|
| 278 | a26b9e8d | Neraud | elif action == 'version': |
| 279 | print('Git commit behind Munin plugin, version {0}'.format(
|
||
| 280 | plugin_version)) |
||
| 281 | elif action == 'update': |
||
| 282 | d079f0fa | Neraud | check_update_repos('cron')
|
| 283 | else: |
||
| 284 | 397052a8 | Neraud | logging.warn("Unknown argument '%s'" % action)
|
| 285 | a26b9e8d | Neraud | sys.exit(1) |
| 286 | else: |
||
| 287 | d079f0fa | Neraud | if conf['update_mode'] == 'munin': |
| 288 | check_update_repos('munin')
|
||
| 289 | 81b129dd | Neraud | print_info() |
