Projet

Général

Profil

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

root / plugins / git / git_commit_behind @ e29c89c0

Historique | Voir | Annoter | Télécharger (9,34 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
if int(os.getenv('MUNIN_DEBUG', 0)) > 0:
128
    logging.basicConfig(level=logging.DEBUG,
129
                        format='%(asctime)s %(levelname)-7s %(message)s')
130

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

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

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

    
152

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

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

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

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

    
171

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

    
183

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

    
188

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

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

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

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

    
214

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

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

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

    
237

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

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

    
253

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

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

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

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