Projet

Général

Profil

Révision 0341e680

ID0341e680b1adfcb4efe29b1d27bef69847f2b765
Parent 14e5261e
Enfant f0479a9a

Ajouté par pcy il y a environ 5 ans

[plugins/shorewall/shorewall_log] added plugin for monitoring shorewall blocks

Graphs the number of blocks done by shorewall, given a specific rule
suffix

Voir les différences:

plugins/shorewall/shorewall_log
1
#!/usr/bin/env python3
2
# -*- python -*-
3

  
4
"""
5

  
6
=head1 NAME
7

  
8
Plugin to monitor iptables logs configured by shorewall
9

  
10
=head1 CONFIGURATION
11

  
12
  logfile: Path to the iptables log file, or "journald" to use journald.
13
           When journalctl exists, the default is "journald", otherwise
14
           "/var/log/kern.log".
15
  journalctlargs: Arguments passed to journalctl to select the right logs.
16
                  The default is "SYSLOG_IDENTIFIER=kernel".
17
  taggroups: Space separated list of groups. A group contains a tag if the
18
             group is substring of the tag. Tags belonging to the same group
19
             will be combined in one graph.
20
  tagfilter: Space separated list of filters. When a tag is matched by a
21
             filter (i.e. if the filter is a substring of the tag) it is
22
             ignored.
23
  prefixformat: The format of the prefix configured in iptables, this is the
24
                LOGFORMAT option in shorewall.conf. When not set the entire
25
                prefix is used.
26
  include_ungrouped: when a tag is found that does not belong to a group,
27
                     make it it's own group
28

  
29
Example:
30

  
31
Using /var/log/kern.log as logfile:
32

  
33
=over 2
34

  
35
  [shorewall_log]
36
  group adm
37
  env.logfile /var/log/kern.log
38

  
39
=back
40

  
41
Using journald:
42

  
43
=over 2
44

  
45
  [shorewall_log]
46
  group systemd-journal
47

  
48
=back
49

  
50
=head1 HISTORY
51

  
52
2017-11-03: v1.0 Bert Van de Poel <bert@bhack.net>: created
53
2020-07-16: v2.0 Vincent Vanlaer <vincenttc@ulyssis.org>: rewrite
54
                 - read all tags from iptables config, instead of the last 24h
55
                   of logs
56
                 - add support for journald
57
                 - use cursors for accuracy
58
                 - convert to multigraph to reduce load
59

  
60
=head1 USAGE
61

  
62
Parameters understood:
63

  
64
  config   (required)
65
  autoconf (optional - used by munin-config)
66

  
67
=head1 MAGIC MARKERS
68

  
69
 #%# family=auto
70
 #%# capabilities=autoconf
71

  
72
=cut
73
"""
74

  
75

  
76
import sys
77
import os
78

  
79

  
80
def autoconf() -> str:
81
    if sys.version_info < (3, 5):
82
        return 'no (This plugin requires python 3.5 or higher)'
83

  
84
    if os.getenv('MUNIN_CAP_MULTIGRAPH', '0') != '1':
85
        return 'no (No multigraph support)'
86

  
87
    import shutil
88

  
89
    if not shutil.which('shorewall'):
90
        return 'no (No shorewall executable found)'
91

  
92
    if not shutil.which('iptables-save'):
93
        return 'no (No iptables-save executable found, required for tag enumeration)'
94

  
95
    return 'yes'
96

  
97

  
98
if len(sys.argv) == 2 and sys.argv[1] == "autoconf":
99
    print(autoconf())
100
    sys.exit()
101

  
102

  
103
from collections import defaultdict, namedtuple
104
from typing import Set, Iterator, TextIO, Dict, Tuple
105
from itertools import takewhile
106
from subprocess import run, PIPE
107
import shlex
108
import shutil
109
import pickle
110
import re
111

  
112

  
113
logfile = os.getenv('logfile', 'journald' if shutil.which('journalctl') else '/var/log/kern.log')
114
journalctl_args = list(shlex.split(os.getenv('journalctlargs',
115
                                             'SYSLOG_IDENTIFIER=kernel')))
116
taggroups = os.getenv('taggroups')
117
taggroups = taggroups.split() if taggroups else []
118
tagfilter = os.getenv('tagfilter')
119
tagfilter = tagfilter.split() if tagfilter else []
120
include_ungrouped = (os.getenv('includeungrouped', 'true').lower() == 'true')
121
prefix_format = os.getenv('prefixformat')
122
if prefix_format:
123
    if sys.version_info < (3, 7):
124
        prefix_format = re.escape(prefix_format).replace('\\%s', '(.+)').replace('\\%d', '\\d+')
125
    else:  # % is no longer escaped
126
        prefix_format = re.escape(prefix_format).replace('%s', '(.+)').replace('%d', '\\d+')
127
    prefix_format = re.compile(prefix_format)
128

  
129

  
130
def get_logtags() -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]:
131
    rules = (run(['iptables-save'], stdout=PIPE, universal_newlines=True).
132
             stdout.splitlines())
133
    tags = defaultdict(set)
134
    groups = defaultdict(set)
135

  
136
    # every line is an iptables rule, in the iptables command/args syntax
137
    # (without the 'iptables' command listed), eg.
138
    #                                    "-A INPUT -p tcp -s 10.3.3.7 -j DROP"
139
    for line in rules:
140
        args = iter(shlex.split(line))
141
        for arg in args:
142
            # we only want rules that log packets, not that accept/drop/...
143
            if arg == '-j' and next(args) != 'LOG':
144
                break
145
            # and we only need to know the logging tag, and add it to the list
146
            if arg == '--log-prefix':
147
                prefix = next(args)
148

  
149
                if prefix_format:
150
                    tag = prefix_format.match(prefix)[1]
151
                else:
152
                    tag = prefix.rstrip()
153

  
154
                if any(ignored in tag for ignored in tagfilter):
155
                    continue
156
                tags[tag].add(prefix)
157

  
158
                for group in taggroups:
159
                    if group in tag:
160
                        groups[group].add(tag)
161
                        break
162
                else:
163
                    if include_ungrouped:
164
                        groups[tag].add(tag)
165

  
166
                break
167

  
168
    return groups, tags
169

  
170

  
171
State = namedtuple('State', ['journal', 'file'])
172

  
173

  
174
def load_state() -> State:
175
    try:
176
        with open(os.getenv('MUNIN_STATEFILE'), 'rb') as f:
177
            return pickle.load(f)
178
    except OSError:
179
        return State(None, None)
180

  
181

  
182
def save_state(state: State):
183
    with open(os.getenv('MUNIN_STATEFILE'), 'wb') as f:
184
        return pickle.dump(state, f)
185

  
186

  
187
def get_lines_journalctl(state: State) -> Iterator[str]:
188
    cursor = state.journal
189

  
190
    def catch_cursor(line: str):
191
        cursor_id = '-- cursor: '
192
        if line.startswith(cursor_id):
193
            save_state(State(line[len(cursor_id):], None))
194
            return False
195
        else:
196
            return True
197

  
198
    if not cursor:  # prevent reading the entire journal on first run
199
        journal = run(['journalctl', '--no-pager', '--quiet', '--lines=0',
200
                       '--show-cursor', *journalctl_args],
201
                      stdout=PIPE, universal_newlines=True)
202
    else:
203
        journal = run(['journalctl', '--no-pager', '--quiet', '--show-cursor',
204
                       '--after-cursor', cursor, *journalctl_args],
205
                      stdout=PIPE, universal_newlines=True)
206

  
207
    yield from filter(catch_cursor, journal.stdout.splitlines())
208

  
209

  
210
def reverse_read(f: TextIO) -> Iterator[str]:
211
    BUFSIZE = 4096
212
    f.seek(0, 2)
213
    position = f.tell()
214
    remainder = ''
215
    while position > 0:
216
        position = max(position - BUFSIZE, 0)
217
        f.seek(position)
218
        lines = f.read(BUFSIZE).splitlines()
219
        lines[-1] += remainder
220
        remainder = lines.pop(0)
221
        yield from reversed(lines)
222
    yield remainder
223

  
224

  
225
def get_lines_logfile(path: str, state: State) -> Iterator[str]:
226
    with open(path, 'r') as f:
227
        cursor = state.file
228

  
229
        reader = reverse_read(f)
230

  
231
        if not cursor:
232
            save_state(State(None, next(reader)))
233
            return
234
        else:
235
            new_cursor = next(reader)
236
            save_state(State(None, new_cursor))
237
            yield new_cursor
238
        yield from takewhile(lambda x: x != cursor, reader)
239

  
240

  
241
def get_tagcount(tags: Dict[str, Set[str]]) -> Dict[str, int]:
242
    count = defaultdict(int)
243
    state = load_state()
244

  
245
    if logfile == 'journald':
246
        lines = get_lines_journalctl(state)
247
        offset = 5
248
    else:
249
        lines = get_lines_logfile(logfile, state)
250
        offset = 6
251

  
252
    for line in lines:
253
        if 'IN=' not in line:
254
            continue
255
        line = line.replace('  ', ' ').split(' ', maxsplit=offset)[-1]
256

  
257
        for tag, prefixes in tags.items():
258
            for prefix in prefixes:
259
                if line.startswith(prefix):
260
                    count[tag] += 1
261
                    break
262
            else:
263
                continue
264
            break
265

  
266
    return count
267

  
268

  
269
def fetch():
270
    groups, logtags = get_logtags()
271
    tagcount = get_tagcount(logtags)
272

  
273
    for group, tags in groups.items():
274
        print('multigraph shorewall_{}'.format(group))
275
        for tag in tags:
276
            print('{}.value {}'.format(tag.lower(), tagcount[tag]))
277

  
278

  
279
def config():
280

  
281
    for group, tags in get_logtags()[0].items():
282
        print('multigraph shorewall_{}'.format(group))
283
        print('graph_title Shorewall Logs for {}'.format(group))
284
        print('graph_vlabel entries per ${graph_period}')
285
        print('graph_category shorewall')
286

  
287
        for tag in sorted(tags):
288
            print('{}.label {}'.format(tag.lower(), tag))
289
            print('{}.type ABSOLUTE'.format(tag.lower()))
290
            print('{}.draw AREASTACK'.format(tag.lower()))
291

  
292

  
293
if len(sys.argv) == 2 and sys.argv[1] == "config":
294
    config()
295
else:
296
    fetch()
297

  
298
# flake8: noqa: E265,E402

Formats disponibles : Unified diff