Projet

Général

Profil

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

root / plugins / git / git_commit_behind @ 646a6c69

Historique | Voir | Annoter | Télécharger (9,4 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
import pwd
118
from random import randint
119
import re
120
from shlex import quote
121
from subprocess import check_output, call, DEVNULL, CalledProcessError
122
import sys
123
import time
124

    
125

    
126
plugin_version = "1.0.0"
127

    
128
if int(os.getenv('MUNIN_DEBUG', 0)) > 0:
129
    logging.basicConfig(level=logging.DEBUG,
130
                        format='%(asctime)s %(levelname)-7s %(message)s')
131

    
132
current_user = pwd.getpwuid(os.geteuid())[0]
133

    
134
conf = {
135
    '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
}
141

    
142
repo_codes = set(re.search('repo\.([^.]+)\..*', elem).group(1)
143
                 for elem in os.environ.keys() if elem.startswith('repo.'))
144

    
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
    print('graph_category file_transfer')
165

    
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
    if not repo_conf['user'] or repo_conf['user'] == current_user:
177
        cmd = [quote(conf['git_path'])] + git_command
178
    else:
179
        shell_cmd = 'cd %s ; %s %s' % (
180
            quote(repo_conf['path']),
181
            quote(conf['git_path']),
182
            ' '.join(git_command))
183
        cmd = ['su', '-', repo_conf['user'], '-s', '/bin/sh', '-c', shell_cmd]
184
    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
def print_info():
193
    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
            logging.error('Error executing git command : %s', e)
213
        except FileNotFoundError as e:
214
            logging.error('Repo not found at path %s' %
215
                          repos_conf[repo_code]['path'])
216

    
217

    
218
def check_update_repos(mode):
219
    if not conf['state_file']:
220
        logging.error('Munin state file unavailable')
221
        sys.exit(1)
222

    
223
    if mode != conf['update_mode']:
224
        logging.debug('Wrong mode, skipping')
225
        return
226

    
227
    if not os.path.isfile(conf['state_file']):
228
        logging.debug('No state file -> updating')
229
        do_update_repos()
230
    elif (os.path.getmtime(conf['state_file']) + conf['update_maxinterval']
231
            < time.time()):
232
        logging.debug('State file last modified too long ago -> updating')
233
        do_update_repos()
234
    elif randint(1, conf['update_probability']) == 1:
235
        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
            logging.error('Error executing git command : %s', e)
248
        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

    
253
    # 'touch' the state file to update its last modified date
254
    Path(conf['state_file']).touch()
255

    
256

    
257
if len(sys.argv) > 1:
258
    action = sys.argv[1]
259
    if action == 'config':
260
        print_config()
261
    elif action == 'autoconf':
262
        errors = []
263

    
264
        if not conf['state_file']:
265
            errors.append('munin state file unavailable')
266

    
267
        if os.access(conf['git_path'], os.X_OK):
268
            test_git = call([conf['git_path'], '--version'], stdout=DEVNULL)
269
            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
        else:
277
            print('yes')
278
    elif action == 'version':
279
        print('Git commit behind Munin plugin, version {0}'.format(
280
            plugin_version))
281
    elif action == 'update':
282
        check_update_repos('cron')
283
    else:
284
        logging.warn("Unknown argument '%s'" % action)
285
        sys.exit(1)
286
else:
287
    if conf['update_mode'] == 'munin':
288
        check_update_repos('munin')
289
    print_info()