Projet

Général

Profil

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

root / plugins / git / git_commit_behind @ e0b243ba

Historique | Voir | Annoter | Télécharger (8,33 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 and slowing down the munin-node,
15
the fetch operation is triggered via a cron job.
16

    
17
=head1 REQUIREMENTS
18

    
19
 - Python3
20
 - Git
21

    
22
=head1 INSTALLATION
23

    
24
Link this plugin, as usual.
25
For example :
26
  ln -s /path/to/git_commit_behind /etc/munin/plugins/git_commit_behind
27

    
28
You also need to setup a cron job to trigger the git fetches.
29

    
30
The plugin can be called with an "update" mode to handle the fetches :
31
munin-run git_commit_behind update <maxinterval> <probability>
32
It will run the fetches randomly (1 in <probability> chances),
33
and ensure that it is run at least every <maxinterval> seconds.
34

    
35
For example, you can use the following cron :
36

    
37
# If the git_commit_behind plugin is enabled, fetch git repositories approx.
38
# once an hour (12 invocations an hour, 1 in 12 chance that the update will
39
# 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 7200 12 >/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 root
48
    env.git_path /path/to/git
49

    
50
user root : required (to be able to switch to each repo user)
51
env.git_path : optional (default : /usr/bin/git), the path to the git binary.
52

    
53
Then, for each repository you want to check, you need the following
54
configuration block under the git_commit_behind section
55
    env.repo.[repoCode].path /path/to/local/repo
56
    env.repo.[repoCode].name Repo Name
57
    env.repo.[repoCode].user user
58
    env.repo.[repoCode].warning 10
59
    env.repo.[repoCode].critical 100
60

    
61
[repoCode] can only contain letters, numbers and underscores.
62

    
63
path : mandatory, the local path to your git repository
64
name : optional (default : [repoCode]), a cleaner name that will be displayed
65
user : optional (default : empty), the owner of the repository
66
       if set and different from the user running the plugin, the git commands
67
       will be executed as this user
68
warning : optional (default 10), the warning threshold
69
critical : optional (default 100), the critical threshold
70

    
71
For example :
72

    
73
    [git_commit_behind]
74
    user root
75

    
76
    env.repo.munin_contrib.path /opt/munin-contrib
77
    env.repo.munin_contrib.name Munin Contrib
78

    
79
    env.repo.other_repo.path /path/to/other-repo
80
    env.repo.other_repo.name Other Repo
81

    
82
=head1 MAGIC MARKERS
83

    
84
  #%# family=auto
85
  #%# capabilities=autoconf
86

    
87
=head1 VERSION
88

    
89
1.0.0
90

    
91
=head1 AUTHOR
92

    
93
Neraud (https://github.com/Neraud)
94

    
95
=head1 LICENSE
96

    
97
GPLv2
98

    
99
=cut"""
100

    
101

    
102
import logging
103
import os
104
from pathlib import Path
105
from random import randint
106
import re
107
from subprocess import check_output, call, DEVNULL, CalledProcessError
108
import sys
109
import time
110

    
111

    
112
plugin_version = "1.0.0"
113

    
114
debug = int(os.getenv('MUNIN_DEBUG', os.getenv('DEBUG', 0))) > 0
115
if debug:
116
    logging.basicConfig(level=logging.DEBUG,
117
                        format='%(asctime)s %(levelname)-7s %(message)s')
118

    
119
conf = {
120
    'git_path':  os.getenv('git_path', '/usr/bin/git'),
121
    'state_file': os.getenv('MUNIN_STATEFILE',
122
                            '/var/lib/munin-node/plugin-state/nobody/' +
123
                            'git_commit_behind.state')
124
}
125

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

    
129
repos_conf = {}
130
for code in repo_codes:
131
    repos_conf[code] = {
132
        'name':        os.getenv('repo.%s.name' % code, code),
133
        'path':        os.getenv('repo.%s.path' % code, None),
134
        'user':        os.getenv('repo.%s.user' % code, None),
135
        'warning':     os.getenv('repo.%s.warning' % code, '10'),
136
        'critical':    os.getenv('repo.%s.critical' % code, '100')
137
    }
138

    
139

    
140
def print_config():
141
    print('graph_title Git repositories - Commits behind')
142

    
143
    print('graph_args --base 1000 -r --lower-limit 0')
144
    print('graph_vlabel number of commits behind')
145
    print('graph_scale yes')
146
    print('graph_info This graph shows the number of commits behind' +
147
          ' for each configured git repository')
148
    print('graph_category system')
149

    
150
    print('graph_order %s' % ' '.join(repo_codes))
151

    
152
    for repo_code in repos_conf.keys():
153
        print('%s.label %s' % (repo_code, repos_conf[repo_code]['name']))
154
        print('%s.warning %s' % (repo_code, repos_conf[repo_code]['warning']))
155
        print('%s.critical %s' %
156
              (repo_code, repos_conf[repo_code]['critical']))
157

    
158

    
159
def generate_git_command(repo_conf, git_command):
160
    if not repo_conf['user'] or repo_conf['user'] == os.environ['USER']:
161
        cmd = [conf['git_path']] + git_command
162
    else:
163
        shell_cmd = 'cd %s ; %s %s' % (
164
            repo_conf['path'], conf['git_path'], ' '.join(git_command))
165
        cmd = ['su', '-', repo_conf['user'], '-c', shell_cmd]
166
    return cmd
167

    
168

    
169
def execute_git_command(repo_conf, git_command):
170
    cmd = generate_git_command(repo_conf, git_command)
171
    return check_output(cmd, cwd=repo_conf['path']).decode('utf-8').rstrip()
172

    
173

    
174
def get_info():
175
    if not os.access(conf['git_path'], os.X_OK):
176
        print('Git (%s) is missing, or not executable !' %
177
              conf['git_path'], file=sys.stderr)
178
        sys.exit(1)
179

    
180
    for repo_code in repos_conf.keys():
181
        logging.debug(' - %s' % repo_code)
182
        try:
183
            remote_branch = execute_git_command(
184
                repos_conf[repo_code],
185
                ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
186
            logging.debug('remote_branch = %s' % remote_branch)
187

    
188
            commits_behind = execute_git_command(
189
                repos_conf[repo_code],
190
                ['rev-list', 'HEAD..%s' % remote_branch, '--count'])
191

    
192
            print('%s.value %d' % (repo_code, int(commits_behind)))
193
        except CalledProcessError as e:
194
            logging.error('Error executing git command : %s', e)
195
        except FileNotFoundError as e:
196
            logging.error('Repo not found at path %s' %
197
                          repos_conf[repo_code]['path'])
198

    
199

    
200
def check_update_repos():
201
    if len(sys.argv) > 2:
202
        max_interval = int(sys.argv[2])
203
    else:
204
        max_interval = 7200
205

    
206
    if len(sys.argv) > 3:
207
        probability = int(sys.argv[3])
208
    else:
209
        probability = 12
210

    
211
    if not os.path.isfile(conf['state_file']):
212
        logging.debug('No state file -> updating')
213
        do_update_repos()
214
    elif os.path.getmtime(conf['state_file']) + max_interval < time.time():
215
        logging.debug('State file last modified too long ago -> updating')
216
        do_update_repos()
217
    elif randint(1, probability) == 1:
218
        logging.debug('Recent state, but random matched -> updating')
219
        do_update_repos()
220
    else:
221
        logging.debug('Recent state and random missed -> skipping')
222

    
223

    
224
def do_update_repos():
225
    for repo_code in repos_conf.keys():
226
        try:
227
            logging.info('Fetching repo %s' % repo_code)
228
            execute_git_command(repos_conf[repo_code], ['fetch'])
229
        except CalledProcessError as e:
230
            logging.error('Error executing git command : %s', e)
231
        except FileNotFoundError as e:
232
            logging.error('Repo not found at path %s' %
233
                          repos_conf[repo_code]['path'])
234
    logging.debug('Updating the state file')
235

    
236
    # 'touch' the state file to update its last modified date
237
    Path(conf['state_file']).touch()
238

    
239

    
240
if len(sys.argv) > 1:
241
    action = sys.argv[1]
242
    if action == 'config':
243
        print_config()
244
    elif action == 'autoconf':
245
        if os.access(conf['git_path'], os.X_OK):
246
            test_git = call([conf['git_path'], '--version'], stdout=DEVNULL)
247
            if test_git == 0:
248
                print('yes')
249
            else:
250
                print('no (git seems to be broken ?!)')
251
        else:
252
            print('no (git is missing or not executable)')
253
    elif action == 'version':
254
        print('Git commit behind Munin plugin, version {0}'.format(
255
            plugin_version))
256
    elif action == 'update':
257
        check_update_repos()
258
    elif action:
259
        logging.warn("Unknown argument '%s'" % action)
260
        sys.exit(1)
261
    else:
262
        get_info()
263
else:
264
    get_info()