|
0 |
- |
1 |
#!/usr/bin/python2.7 |
1 |
-- |
2 |
# coding: utf-8 |
|
|
3 |
# |
4 |
# Univention Management Console module: |
5 |
# System Diagnosis UMC module |
6 |
# |
7 |
# Copyright 2017 Univention GmbH |
8 |
# |
9 |
# http://www.univention.de/ |
10 |
# |
11 |
# All rights reserved. |
12 |
# |
13 |
# The source code of this program is made available |
14 |
# under the terms of the GNU Affero General Public License version 3 |
15 |
# (GNU AGPL V3) as published by the Free Software Foundation. |
16 |
# |
17 |
# Binary versions of this program provided by Univention to you as |
18 |
# well as other copyrighted, protected or trademarked materials like |
19 |
# Logos, graphics, fonts, specific documentations and configurations, |
20 |
# cryptographic keys etc. are subject to a license agreement between |
21 |
# you and Univention and not subject to the GNU AGPL V3. |
22 |
# |
23 |
# In the case you use this program under the terms of the GNU AGPL V3, |
24 |
# the program is provided in the hope that it will be useful, |
25 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
26 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
27 |
# GNU Affero General Public License for more details. |
28 |
# |
29 |
# You should have received a copy of the GNU Affero General Public |
30 |
# License with the Debian GNU/Linux or Univention distribution in file |
31 |
# /usr/share/common-licenses/AGPL-3; if not, see |
32 |
# <http://www.gnu.org/licenses/>. |
33 |
|
34 |
import re |
35 |
import mmap |
36 |
import errno |
37 |
import socket |
38 |
import itertools as it |
39 |
|
40 |
from univention.management.console.modules.diagnostic import Warning |
41 |
|
42 |
from univention.lib.i18n import Translation |
43 |
_ = Translation('univention-management-console-module-diagnostic').translate |
44 |
|
45 |
title = _('Check logfiles for errors') |
46 |
description = _('No errors found in logfiles.') |
47 |
|
48 |
|
49 |
class Logcheck(object): |
50 |
# The `error_regex` should be a `re.compile()`ed regex matching error |
51 |
# markers in log messages. Log messages are either single log lines, or |
52 |
# multi line log entries, if `split_messages()` is overridden (as in |
53 |
# `MultiLineLogcheck`). This is used as `error_regex.search()` to find log |
54 |
# messages that contain any error marker. |
55 |
# example: `ERROR_REGEX = re.compile('(ERROR)|(CRITICAL)')` |
56 |
ERROR_REGEX = None |
57 |
|
58 |
# The `start_regex` should be a `re.compile()`ed regex matching the start |
59 |
# of a service. This should include the start of the line and not only the |
60 |
# start marker. This is used as `start_regex.finditer()` to find the last |
61 |
# service start in a logfile and seek to it. |
62 |
# example: `START_REGEX = re.compile('^.*Starting service xxx')` |
63 |
START_REGEX = None |
64 |
|
65 |
def __init__(self, filename, error_regex=None, start_regex=None): |
66 |
self.filename = filename |
67 |
self.error_regex = error_regex or self.ERROR_REGEX |
68 |
self.start_regex = start_regex or self.START_REGEX |
69 |
|
70 |
self.fob = open(self.filename) |
71 |
# We use a `mmap`, as we can't use `re` on files and reading all the |
72 |
# content of a file might by too slow. |
73 |
self.mmap = mmap.mmap(self.fob.fileno(), 0, access=mmap.ACCESS_READ) |
74 |
|
75 |
if self.start_regex is not None: |
76 |
self._seek_to_last_start() |
77 |
|
78 |
@classmethod |
79 |
def with_file(cls, filename, **kwargs): |
80 |
def wrapper(): |
81 |
return cls(filename, **kwargs) |
82 |
return wrapper |
83 |
|
84 |
@classmethod |
85 |
def with_error_phrases(cls, filename, *phrases, **kwargs): |
86 |
regex = re.compile('|'.join('({})'.format(phrase) for phrase in phrases)) |
87 |
return cls.with_file(filename, error_regex=regex, **kwargs) |
88 |
|
89 |
def __enter__(self): |
90 |
return self |
91 |
|
92 |
def __exit__(self): |
93 |
self.close() |
94 |
|
95 |
def _seek_to_last_start(self): |
96 |
# find the last `match` object by exhausting the iterator |
97 |
match = None |
98 |
for match in self.start_regex.finditer(self.mmap): |
99 |
pass |
100 |
start = match.start() if match is not None else 0 |
101 |
self.mmap.seek(start) |
102 |
|
103 |
def close(self): |
104 |
if self.mmap is not None: |
105 |
self.mmap.close() |
106 |
if self.fob is not None: |
107 |
self.fob.close() |
108 |
|
109 |
def split_messages(self): |
110 |
''' |
111 |
Yield all lines like `file.splitlines()`. |
112 |
Override this for multiline log entries. |
113 |
''' |
114 |
while True: |
115 |
line = self.mmap.readline() |
116 |
if line == '': |
117 |
break |
118 |
yield line.rstrip('\n') |
119 |
|
120 |
def errors(self): |
121 |
for message in self.split_messages(): |
122 |
search = self.error_regex.search(message) |
123 |
if search is not None and search.span() != (0, 0): |
124 |
yield message |
125 |
|
126 |
|
127 |
class MultiLineLogcheck(Logcheck): |
128 |
# The `message_start_regex` should be a `re.compile()`ed regex matching the |
129 |
# start of a multiline log message. This is used to group together multiple |
130 |
# log lines and used as `message_start_regex.search()` to find if a line is |
131 |
# a message start. |
132 |
MESSAGE_START_REGEX = None |
133 |
|
134 |
def __init__(self, filename, message_start_regex=None, **kwargs): |
135 |
super(MultiLineLogcheck, self).__init__(filename, **kwargs) |
136 |
self.message_start_regex = message_start_regex or self.MESSAGE_START_REGEX |
137 |
|
138 |
def split_messages(self): |
139 |
chunk = list() |
140 |
for line in super(MultiLineLogcheck, self).split_messages(): |
141 |
if self.message_start_regex.search(line) is not None: |
142 |
if chunk: |
143 |
yield '\n'.join(chunk) |
144 |
chunk = list() |
145 |
chunk.append(line) |
146 |
if chunk: |
147 |
yield '\n'.join(chunk) |
148 |
|
149 |
|
150 |
class SyslogLogCheck(Logcheck): |
151 |
SERVICE_NAME = None |
152 |
|
153 |
def __init__(self, filename, service_name=None, **kwargs): |
154 |
super(SyslogLogCheck, self).__init__(filename, **kwargs) |
155 |
self.service_name = service_name or self.SERVICE_NAME |
156 |
|
157 |
def split_messages(self): |
158 |
if self.service_name is not None: |
159 |
regex_tmpl = '^.*({})|(unassigned-hostname) {}\[\d+\]:' |
160 |
service_regex = re.compile(regex_tmpl.format(socket.gethostname(), self.service_name)) |
161 |
for line in super(SyslogLogCheck, self).split_messages(): |
162 |
if service_regex.search(line) is not None: |
163 |
yield line |
164 |
else: |
165 |
for line in super(SyslogLogCheck, self).split_messages(): |
166 |
yield line |
167 |
|
168 |
|
169 |
class SambaLogcheck(MultiLineLogcheck): |
170 |
# comma separated logging header (level is not reliable): |
171 |
# <date>, <level> [, pid=<pid>] [, <effective user>, <real user>] |
172 |
# see samba source: lib/util/debug.c `dbghdrclass()` |
173 |
MESSAGE_START_REGEX = re.compile(( |
174 |
'^\[' |
175 |
'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}.\d{6}' |
176 |
', ( \d)|(\d{2}).' |
177 |
'(, pid=\d+)?' |
178 |
'(, effective\(\d+, \d+\)), real\(\d+, \d+\)?' |
179 |
'\]' |
180 |
)) |
181 |
|
182 |
|
183 |
class UniventionDebugLogcheck(Logcheck): |
184 |
ERROR_REGEX = re.compile('\(\s*ERROR\s*\)') |
185 |
START_REGEX = re.compile('^.*DEBUG_INIT.*$', re.MULTILINE) |
186 |
|
187 |
@classmethod |
188 |
def with_levels(cls, filename, *levels, **kwargs): |
189 |
phrases = ('\(\s*{}\s*\)'.format(l) for l in levels) |
190 |
return cls.with_error_phrases(filename, *phrases, **kwargs) |
191 |
|
192 |
|
193 |
class MultiLineUniventionDebugLogcheck(MultiLineLogcheck, UniventionDebugLogcheck): |
194 |
MESSAGE_START_REGEX = re.compile('^\d{2}.\d{2}.\d{4} \d{2}:\d{2}:\d{2},\d{3}') |
195 |
|
196 |
|
197 |
class UniventionUpdaterLogcheck(UniventionDebugLogcheck): |
198 |
ERROR_REGEX = re.compile('(CRITICAL)|(error code)|(Hash Sum mismatch)') |
199 |
START_REGEX = re.compile('^Starting univention-upgrade. Current UCS version is', re.MULTILINE) |
200 |
|
201 |
|
202 |
LOGFILES = { |
203 |
'univention/listener': |
204 |
UniventionDebugLogcheck.with_file('/var/log/univention/listener.log'), |
205 |
'univention/notifier': |
206 |
UniventionDebugLogcheck.with_file('/var/log/univention/notifier.log'), |
207 |
'univention/updater': |
208 |
UniventionUpdaterLogcheck.with_file('/var/log/univention/updater.log'), |
209 |
'univention/s4connector': |
210 |
MultiLineUniventionDebugLogcheck.with_levels('/var/log/univention/connector-s4.log', |
211 |
'ERROR', 'WARNING'), |
212 |
'syslog/named': |
213 |
SyslogLogCheck.with_file('/var/log/syslog', service_name='named', |
214 |
error_regex=re.compile("update '.*' denied")), |
215 |
'samba/samba': |
216 |
SambaLogcheck.with_error_phrases('/var/log/samba/log.samba', 'BACKTRACE'), |
217 |
'samba/smbd': |
218 |
SambaLogcheck.with_error_phrases('/var/log/samba/log.smbd', 'BACKTRACE'), |
219 |
} |
220 |
|
221 |
|
222 |
class ErrorFound(Exception): |
223 |
def __init__(self, name, filename, error_message): |
224 |
super(ErrorFound, self).__init__(name, filename, error_message) |
225 |
self.name = name |
226 |
self.filename = filename |
227 |
self.error_message = error_message |
228 |
|
229 |
def __str__(self): |
230 |
return self.error_message |
231 |
|
232 |
|
233 |
def find_log_errors(): |
234 |
for (name, check_wrapper) in LOGFILES.iteritems(): |
235 |
try: |
236 |
check = check_wrapper() |
237 |
except IOError as error: |
238 |
if error.errno != errno.ENOENT: |
239 |
raise |
240 |
else: |
241 |
for error in check.errors(): |
242 |
yield ErrorFound(name, check.filename, error) |
243 |
|
244 |
|
245 |
def run(): |
246 |
errors = list(find_log_errors()) |
247 |
if errors: |
248 |
ed = [_('Some errors in logfiles found.')] |
249 |
grouped = it.groupby(errors, lambda e: (e.name, e.filename)) |
250 |
for ((name, filename), group) in grouped: |
251 |
ed.append(_('\nIn logfile {log!r}:').format(log=filename)) |
252 |
ed.extend(str(e) for e in group) |
253 |
raise Warning(description='\n'.join(ed)) |
254 |
|
255 |
|
256 |
if __name__ == '__main__': |
257 |
from univention.management.console.modules.diagnostic import main |
258 |
main() |
2 |
.../umc/python/diagnostic/de.po | 24 ++++++++++++++++++++-- |
259 |
.../umc/python/diagnostic/de.po | 24 ++++++++++++++++++++-- |
3 |
1 file changed, 22 insertions(+), 2 deletions(-) |
260 |
1 file changed, 22 insertions(+), 2 deletions(-) |