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
#! /usr/bin/env python3
2

    
3
"""=cut
4
=head1 NAME
5

    
6
git_commit_behind - Munin plugin to monitor local git repositories and report how many commits behind their remote they are
7

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

    
19
=head1 REQUIREMENTS
20

    
21
=over 4
22

    
23
=item Python3
24

    
25
=item Git
26

    
27
=back
28

    
29

    
30
=head1 INSTALLATION
31

    
32
Link this plugin, as usual.
33
For example :
34

    
35
  ln -s /path/to/git_commit_behind /etc/munin/plugins/git_commit_behind
36

    
37
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

    
40
For example, you can use the following cron :
41

    
42
 # 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

    
49
=head1 CONFIGURATION
50

    
51
Use your "/etc/munin/plugin-conf.d/munin-node" to configure this plugin.
52

    
53
    [git_commit_behind]
54
    user [user]
55
    env.git_path /path/to/git
56
    env.update.mode [munin|cron]
57
    env.update.probability 12
58
    env.update.maxinterval 7200
59

    
60
user [user] : required, the owner of the repository checkouts
61
    in case of multiple different owners, use root
62
env.git_path : optional (default : /usr/bin/git), the path to the git binary.
63
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

    
72
Then, for each repository you want to check, you need the following
73
configuration block under the git_commit_behind section:
74

    
75
    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
from pathlib import Path
125
import pwd
126
from random import randint
127
import re
128
from shlex import quote
129
from subprocess import check_output, call, DEVNULL, CalledProcessError
130
import sys
131
import time
132

    
133

    
134
plugin_version = "1.0.0"
135

    
136
if int(os.getenv('MUNIN_DEBUG', 0)) > 0:
137
    logging.basicConfig(level=logging.DEBUG,
138
                        format='%(asctime)s %(levelname)-7s %(message)s')
139

    
140
current_user = pwd.getpwuid(os.geteuid())[0]
141

    
142
conf = {
143
    '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
}
149

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

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

    
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
    if not repo_conf['user'] or repo_conf['user'] == current_user:
185
        cmd = [quote(conf['git_path'])] + git_command
186
    else:
187
        shell_cmd = 'cd %s ; %s %s' % (
188
            quote(repo_conf['path']),
189
            quote(conf['git_path']),
190
            ' '.join(git_command))
191
        cmd = ['su', '-', repo_conf['user'], '-s', '/bin/sh', '-c', shell_cmd]
192
    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
def print_info():
201
    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
            logging.error('Error executing git command : %s', e)
221
        except FileNotFoundError as e:
222
            logging.error('Repo not found at path %s' %
223
                          repos_conf[repo_code]['path'])
224

    
225

    
226
def check_update_repos(mode):
227
    if not conf['state_file']:
228
        logging.error('Munin state file unavailable')
229
        sys.exit(1)
230

    
231
    if mode != conf['update_mode']:
232
        logging.debug('Wrong mode, skipping')
233
        return
234

    
235
    if not os.path.isfile(conf['state_file']):
236
        logging.debug('No state file -> updating')
237
        do_update_repos()
238
    elif (os.path.getmtime(conf['state_file']) + conf['update_maxinterval']
239
            < time.time()):
240
        logging.debug('State file last modified too long ago -> updating')
241
        do_update_repos()
242
    elif randint(1, conf['update_probability']) == 1:
243
        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
            logging.error('Error executing git command : %s', e)
256
        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

    
261
    # 'touch' the state file to update its last modified date
262
    Path(conf['state_file']).touch()
263

    
264

    
265
if len(sys.argv) > 1:
266
    action = sys.argv[1]
267
    if action == 'config':
268
        print_config()
269
    elif action == 'autoconf':
270
        errors = []
271

    
272
        if not conf['state_file']:
273
            errors.append('munin state file unavailable')
274

    
275
        if os.access(conf['git_path'], os.X_OK):
276
            test_git = call([conf['git_path'], '--version'], stdout=DEVNULL)
277
            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
        else:
285
            print('yes')
286
    elif action == 'version':
287
        print('Git commit behind Munin plugin, version {0}'.format(
288
            plugin_version))
289
    elif action == 'update':
290
        check_update_repos('cron')
291
    else:
292
        logging.warn("Unknown argument '%s'" % action)
293
        sys.exit(1)
294
else:
295
    if conf['update_mode'] == 'munin':
296
        check_update_repos('munin')
297
    print_info()