Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

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()