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:
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user