Projet

Général

Profil

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

root / plugins / git / git_commit_behind @ 0d538451

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

    
124

    
125
plugin_version = "1.0.0"
126

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

    
132
conf = {
133
    'git_path':            os.getenv('git_path', '/usr/bin/git'),
134
    'state_file':          os.getenv('MUNIN_STATEFILE'),
135
    'update_mode':         os.getenv('update.mode', 'munin'),
136
    'update_probability':  int(os.getenv('update.probability', '12')),
137
    'update_maxinterval':  int(os.getenv('update.maxinterval', '7200'))
138
}
139

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

    
143
repos_conf = {}
144
for code in repo_codes:
145
    repos_conf[code] = {
146
        'name':        os.getenv('repo.%s.name' % code, code),
147
        'path':        os.getenv('repo.%s.path' % code, None),
148
        'user':        os.getenv('repo.%s.user' % code, None),
149
        'warning':     os.getenv('repo.%s.warning' % code, '10'),
150
        'critical':    os.getenv('repo.%s.critical' % code, '100')
151
    }
152

    
153

    
154
def print_config():
155
    print('graph_title Git repositories - Commits behind')
156

    
157
    print('graph_args --base 1000 -r --lower-limit 0')
158
    print('graph_vlabel number of commits behind')
159
    print('graph_scale yes')
160
    print('graph_info This graph shows the number of commits behind' +
161
          ' for each configured git repository')
162
    print('graph_category system')
163

    
164
    print('graph_order %s' % ' '.join(repo_codes))
165

    
166
    for repo_code in repos_conf.keys():
167
        print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
168
        print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
169
        print('%s.critical %s' %
170
              (repo_code, repos_conf[repo_code]['critical']))
171

    
172

    
173
def generate_git_command(repo_conf, git_command):
174
    if not repo_conf['user'] or repo_conf['user'] == os.environ['USER']:
175
        cmd = [quote(conf['git_path'])] + git_command
176
    else:
177
        shell_cmd = 'cd %s ; %s %s' % (
178
            quote(repo_conf['path']),
179
            quote(conf['git_path']),
180
            ' '.join(git_command))
181
        cmd = ['su', '-', repo_conf['user'], '-c', shell_cmd]
182
    return cmd
183

    
184

    
185
def execute_git_command(repo_conf, git_command):
186
    cmd = generate_git_command(repo_conf, git_command)
187
    return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
188

    
189

    
190
def print_info():
191
    if not os.access(conf['git_path'], os.X_OK):
192
        print('Git (%s) is missing, or not executable !' %
193
              conf['git_path'], file=sys.stderr)
194
        sys.exit(1)
195

    
196
    for repo_code in repos_conf.keys():
197
        logging.debug(' - %s' % repo_code)
198
        try:
199
            remote_branch = execute_git_command(
200
                repos_conf[repo_code],
201
                ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
202
            logging.debug('remote_branch = %s' % remote_branch)
203

    
204
            commits_behind = execute_git_command(
205
                repos_conf[repo_code],
206
                ['rev-list', 'HEAD..%s' % remote_branch, '--count'])
207

    
208
            print('%s.value %d' % (repo_code, int(commits_behind)))
209
        except CalledProcessError as e:
210
            logging.error('Error executing git command : %s', e)
211
        except FileNotFoundError as e:
212
            logging.error('Repo not found at path %s' %
213
                          repos_conf[repo_code]['path'])
214

    
215

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

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

    
225
    if not os.path.isfile(conf['state_file']):
226
        logging.debug('No state file -> updating')
227
        do_update_repos()
228
    elif (os.path.getmtime(conf['state_file']) + conf['update_maxinterval']
229
            < time.time()):
230
        logging.debug('State file last modified too long ago -> updating')
231
        do_update_repos()
232
    elif randint(1, conf['update_probability']) == 1:
233
        logging.debug('Recent state, but random matched -> updating')
234
        do_update_repos()
235
    else:
236
        logging.debug('Recent state and random missed -> skipping')
237

    
238

    
239
def do_update_repos():
240
    for repo_code in repos_conf.keys():
241
        try:
242
            logging.info('Fetching repo %s' % repo_code)
243
            execute_git_command(repos_conf[repo_code], ['fetch'])
244
        except CalledProcessError as e:
245
            logging.error('Error executing git command : %s', e)
246
        except FileNotFoundError as e:
247
            logging.error('Repo not found at path %s' %
248
                          repos_conf[repo_code]['path'])
249
    logging.debug('Updating the state file')
250

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

    
254

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

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

    
265
        if os.access(conf['git_path'], os.X_OK):
266
            test_git = call([conf['git_path'], '--version'], stdout=DEVNULL)
267
            if test_git != 0:
268
                errors.append('git seems to be broken ?!')
269
        else:
270
            errors.append('git is missing or not executable')
271

    
272
        if errors:
273
            print('no (%s)' % ', '.join(errors))
274
        else:
275
            print('yes')
276
    elif action == 'version':
277
        print('Git commit behind Munin plugin, version {0}'.format(
278
            plugin_version))
279
    elif action == 'update':
280
        check_update_repos('cron')
281
    else:
282
        logging.warn("Unknown argument '%s'" % action)
283
        sys.exit(1)
284
else:
285
    if conf['update_mode'] == 'munin':
286
        check_update_repos('munin')
287
    print_info()