2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-08-28 17:46:42 +00:00

Merge pull request #275 from Obihoernchen/feature/eventlog-sources

Add nodeeventlog source filtering for redfish
This commit is contained in:
Jarrod Johnson
2026-08-19 09:25:55 -04:00
committed by GitHub
4 changed files with 152 additions and 19 deletions
+62 -2
View File
@@ -53,7 +53,13 @@ argparser.add_option('-t', '--timeframe', type='string',
'entries from the last hours or days. '
'1h would be one hour, 4d would be four days. '
'format <num>h or <num>d'
)
)
argparser.add_option('-s', '--source', type='string',
help='only show entries from the named logs, comma '
'delimited. "-s list" names the logs this node '
'offers instead of showing entries')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -72,6 +78,19 @@ if len(args) == 2:
argparser.print_help()
sys.exit(1)
listmode = bool(options.source) and options.source.lower() == 'list'
wantsources = None
if options.source and not listmode:
wantsources = set(x.strip().lower() for x in options.source.split(',')
if x.strip())
if options.source and deletemode:
# Clearing a subset is not something the platforms offer, and clearing more
# than was asked for is not something to do quietly
sys.stderr.write(
'Clearing a selection of logs is not supported, so --source cannot be '
'combined with clear\n')
sys.exit(1)
session = client.Command()
exitcode = 0
@@ -125,6 +144,7 @@ if options.timeframe:
sys.exit(1)
timeframe = dt.now() - tdelta
sources_seen = {}
event_dict = {}
nodes = []
for res in session.read('/noderange/{0}/nodes/'.format(args[0])):
@@ -145,6 +165,17 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
exitcode |= 1
if 'events' in thisdata:
evtdata = thisdata['events']
for evt in evtdata:
if evt.get('log_id', None):
sources_seen.setdefault(node, set()).add(evt['log_id'])
if listmode:
continue
if wantsources is not None:
# Filter before any tail is taken, or asking for the last
# few entries of one log would answer with fewer
evtdata = [x for x in evtdata if (x.get('log_id', '')
or '').lower()
in wantsources]
if options.lines:
event_dict[node].extend(evtdata)
else:
@@ -158,7 +189,34 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
else:
print('{0}: {1}'.format(node, format_event(evt)))
if options.lines:
if listmode:
for node in nodes:
found = sorted(sources_seen.get(node, ()))
if found:
print('{0}: {1}'.format(node, ','.join(found)))
else:
sys.stderr.write(
'No event log sources reported for "{0}"\n'.format(node))
elif wantsources is not None:
for node in nodes:
found = set(x.lower() for x in sources_seen.get(node, ()))
missing = wantsources - found
if not missing:
continue
if found:
sys.stderr.write('{0}: no such log source: {1}, this node has {2}\n'
.format(node, ','.join(sorted(missing)),
','.join(sorted(sources_seen[node]))))
else:
# A log that holds no entries names no source, so an empty answer
# here is as often a quiet platform as a wrong name. Either way
# saying nothing would look like the log simply had nothing in it.
sys.stderr.write(
'{0}: no entries from any log source, so nothing could match '
'{1}\n'.format(node, ','.join(sorted(missing))))
exitcode |= 1
if options.lines and not listmode:
for node in nodes:
evtdata_list = event_dict[node]
if len(evtdata_list) != 0:
@@ -172,3 +230,5 @@ if options.lines:
print('{0}: {1}'.format(node, format_event(evt)))
else:
print('{0}: {1}'.format(node, format_event(evt)))
sys.exit(exitcode)
+32 -1
View File
@@ -3,13 +3,28 @@ nodeeventlog(8) -- Pull eventlog from confluent nodes
## SYNOPSIS
`nodeeventlog [options] <noderange> [clear]`
`nodeeventlog [options] [-s <sources>] <noderange> [clear]`
## DESCRIPTION
`nodeeventlog` pulls and optionally clears the event log from the requested
noderange.
A platform may keep its events in more than one log, and some of those are
noisier than others. Reading gathers every log the platform describes as an
event log, wherever it keeps them, and `-s` narrows the output to the ones
asked for. `-s list` names the logs a node offers rather than showing entries.
A source is named by the entries that come from it, so a log that holds no
entries is not among them, and a node whose logs are all empty names none. An
ipmi managed node keeps its events in the one log the spec gives it, which is
reported as `SEL`.
Clearing is deliberately narrower than reading: it only touches the logs the
platform's own manager publishes, so a log that only a read reaches is never
destroyed by one. A selection cannot be cleared, so `-s` and `clear` are
refused together.
## OPTIONS
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
@@ -25,10 +40,26 @@ noderange.
entries from within the last one hour.
* `-s SOURCES`, `--source=SOURCES`:
only show entries from the named logs, comma delimited and matched without
regard to case. `-s list` names the logs the node offers instead of showing
entries. A platform that named a log "list" would be shadowed by that.
Asking for a log the node named nothing from is reported on stderr and
exits non-zero, rather than looking like an empty log.
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Ask which logs a node keeps:
`# nodeeventlog n2 -s list`
`n2: AuditLog,EventLog,Logs,SEL`
* Pull only the sel and the chassis log, leaving the audit noise out:
`# nodeeventlog n2 -s SEL,Logs`
`n2: 07/23/2026 13:03:12 SEL: Processor - Thermal Trip`
`n2: 08/05/2026 04:30:26 Logs: PowerUnit - Power Off`
* Pull the event log from n2 and n3:
`# nodeeventlog n2,n3`
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
+54 -16
View File
@@ -570,18 +570,51 @@ class OEMHandler(object):
@classmethod
def is_event_log(cls, loginfo):
"""Say whether a log service holds events rather than something else"""
identity = '{0} {1}'.format(loginfo.get('Id', ''),
loginfo.get('Name', '')).lower()
for word in cls.noneventlogwords:
if word in identity:
return False
for part in (loginfo.get('Id', ''), loginfo.get('Name', '')):
# A build is free to space or punctuate the same name differently,
# "BIOS POST Code Log" for a post code log, so compare without any
# of that rather than miss it and merge 2719 post codes into the
# event log
flat = re.sub(r'[^a-z0-9]', '', str(part).lower())
for word in cls.noneventlogwords:
if word in flat:
return False
return True
async def _othereventlogcollections(self, fishclient, everywhere=True):
"""The log service collections outside the manager.
A platform is free to keep its event log on the system or the chassis
rather than on the manager, and more than one of those may link the very
same collection, so the answer is deduplicated.
"""
collections = []
candidates = list(self._allsysurls)
if everywhere:
try:
chassiscol = await self._do_web_request('/redfish/v1/Chassis')
candidates.extend(
x['@odata.id'] for x in chassiscol.get('Members', []))
except Exception:
# A platform that will not describe its chassis still has the
# rest of its logs to offer
pass
for url in candidates:
try:
info = await self._do_web_request(url)
except Exception:
continue
lscollection = info.get('LogServices', {}).get('@odata.id', None)
if lscollection and lscollection not in collections:
collections.append(lscollection)
return collections
async def get_event_log(self, clear=False, fishclient=None, extraurls=[]):
bmcinfo = await self._do_web_request(await fishclient.get_bmcurl())
# A manager need not publish log services at all, and one that does not
# is the clearest case of a platform keeping its event log elsewhere, so
# carry on to the fallback below rather than answering with nothing
lsurl = bmcinfo.get('LogServices', {}).get('@odata.id', None)
if not lsurl:
return
currtime = bmcinfo.get('DateTime', None)
correction = timedelta(0)
utz = tz.tzoffset('', 0)
@@ -612,17 +645,22 @@ class OEMHandler(object):
found.append(candidate)
return found
lurls = await eventlogurls(lsurl)
if not lurls:
lurls = await eventlogurls(lsurl) if lsurl else []
if not lurls or not clear:
# Some implementations keep no event log under the manager and put
# it under the system instead, so fall back to looking there rather
# than answering with nothing at all.
for sysurl in self._allsysurls:
currsysinfo = await self._do_web_request(sysurl)
syslsurl = currsysinfo.get('LogServices', {}).get(
'@odata.id', None)
if syslsurl:
lurls.extend(await eventlogurls(syslsurl))
# than answering with nothing at all. A read goes further and
# gathers every event log the platform publishes, since one that is
# never read is one nobody can act on. Clearing deliberately does
# not: a log that only a read reaches must not be destroyed by one.
for lscollection in await self._othereventlogcollections(
fishclient, everywhere=not clear):
if lscollection == lsurl:
# A platform may link the manager's own collection from the
# system and the chassis as well, and reading it three times
# would answer the same thing three times
continue
lurls.extend(await eventlogurls(lscollection))
lurls.extend([x['@odata.id'] for x in extraurls])
seenurls = set()
for lurl in lurls:
@@ -933,6 +933,10 @@ class IpmiHandler:
await self.output.put(msg.EventCollection(eventout, name=self.node))
def pyghmi_event_to_confluent(self, event):
# An ipmi platform keeps its events in one place and the spec has a
# name for it, so say so rather than leaving the source blank and
# having a caller asking for a named log get nothing back
event.setdefault('log_id', 'SEL')
event['severity'] = _str_health(event.get('severity', 'unknown'))
if 'event_data' in event:
event['event'] = '{0} - {1}'.format(