split monitors tab and surface boot type and nic link state
Tracked Services and Tracked Processes were sharing a tab with system health cards, so what is running on the box and how the box itself is doing read as one list. They now sit in a Service/Process Monitors tab and the old tab is System Monitors. The group vocabulary is duplicated across the renderer, the tab map and the panel declarations with nothing checking they agree, and a group named in one but not the others is dropped in silence - that has eaten cards twice. Tests now pin the vocabulary on both sides. Boot type and NIC link state were collected in Phase 2 but shown nowhere. Boot type only renders when fast startup applies, because uptime measured from a resume understates how long the kernel has been up and that is the case worth naming. Link state leaves a disconnected NIC unknown rather than half duplex: live hardware returns empty speed and duplex when unplugged, and half duplex is the cable fault signature this column exists to catch.
This commit is contained in:
@@ -570,6 +570,15 @@ def panel_hardware(asset_id: int):
|
||||
return success_response({'fields': fields})
|
||||
|
||||
|
||||
# Wire values to operator-readable text. The wire keys are stable contract;
|
||||
# these labels are not, so they live here rather than in the agent.
|
||||
_REBOOT_REASON_LABELS = {
|
||||
'componentbasedservicing': 'servicing',
|
||||
'windowsupdate': 'Windows Update',
|
||||
'pendingfilerename': 'files staged for replacement',
|
||||
}
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/osinfo', methods=['GET'])
|
||||
@require_permission('endpoints.view')
|
||||
def panel_osinfo(asset_id: int):
|
||||
@@ -584,10 +593,31 @@ def panel_osinfo(asset_id: int):
|
||||
|
||||
osinfo = _state_json(state, 'osinfojson') or {}
|
||||
bios = _state_json(state, 'biosjson') or {}
|
||||
bootinfo = _state_json(state, 'bootinfojson') or {}
|
||||
|
||||
pendingreboot = None
|
||||
if state.pendingreboot is not None:
|
||||
pendingreboot = 'Yes' if state.pendingreboot else 'No'
|
||||
# Say WHICH signal fired when we know. A staged file rename means the
|
||||
# box is mid-servicing; a lone servicing key can sit set for weeks on a
|
||||
# machine nobody rebooted. Same "Yes", very different urgency.
|
||||
reasons = _state_json(state, 'rebootreasonsjson')
|
||||
if state.pendingreboot and isinstance(reasons, list) and reasons:
|
||||
pendingreboot = '{} ({})'.format(
|
||||
pendingreboot, ', '.join(_REBOOT_REASON_LABELS.get(r, r)
|
||||
for r in reasons if isinstance(r, str)))
|
||||
|
||||
# Only worth a row when fast startup actually applies. On a box with it
|
||||
# disabled and a cold last boot there is nothing to warn about, and a
|
||||
# permanent "Cold" row is noise on a card that is already dense.
|
||||
boottype = bootinfo.get('lastboottype')
|
||||
if boottype in (None, '', 'Cold', 'Unknown'):
|
||||
boottype = None
|
||||
elif boottype == 'Fast startup':
|
||||
# Uptime is measured from the resume, not the last true boot, so it
|
||||
# understates how long the kernel has actually been up. Said plainly
|
||||
# because this is the field that makes uptime read wrong.
|
||||
boottype = 'Fast startup (uptime not since cold boot)'
|
||||
|
||||
fields = [
|
||||
{'label': 'OS version', 'value': osinfo.get('version'), 'mono': True},
|
||||
@@ -598,6 +628,7 @@ def panel_osinfo(asset_id: int):
|
||||
{'label': 'BIOS vendor', 'value': bios.get('vendor')},
|
||||
{'label': 'BIOS version', 'value': bios.get('version'), 'mono': True},
|
||||
{'label': 'BIOS date', 'value': _format_agent_date(bios.get('releasedate'))},
|
||||
{'label': 'Last boot', 'value': boottype},
|
||||
{'label': 'Pending reboot', 'value': pendingreboot},
|
||||
]
|
||||
fields = [f for f in fields
|
||||
@@ -1218,6 +1249,32 @@ def panel_activity(asset_id: int):
|
||||
} for r in rows[:15]])
|
||||
|
||||
|
||||
def _link_label(speedbps, fullduplex):
|
||||
"""Render NIC link speed and duplex as one cell.
|
||||
|
||||
Null speed means the agent could not read link state OR the adapter is
|
||||
disconnected - verified on a live box that a disconnected NIC returns empty
|
||||
values. Either way we do not know, so it reads '-' rather than 0 Mb.
|
||||
|
||||
Half duplex is called out explicitly because that is the actionable case: a
|
||||
NIC that negotiated half duplex on a switch port is a cable or port fault
|
||||
that otherwise presents as "the machine is slow sometimes". Full duplex is
|
||||
the normal case and gets no annotation, so the only thing standing out in
|
||||
the column is the thing worth looking at.
|
||||
"""
|
||||
if not speedbps:
|
||||
return '-'
|
||||
|
||||
if speedbps >= 1000000000:
|
||||
speed = '{:g} Gb'.format(speedbps / 1000000000)
|
||||
else:
|
||||
speed = '{:g} Mb'.format(speedbps / 1000000)
|
||||
|
||||
if fullduplex is False:
|
||||
return '{} half duplex'.format(speed)
|
||||
return speed
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/interfaces', methods=['GET'])
|
||||
@require_permission('endpoints.view')
|
||||
def panel_interfaces(asset_id: int):
|
||||
@@ -1255,6 +1312,8 @@ def panel_interfaces(asset_id: int):
|
||||
# A gatewayless NIC with an address is the machine link; the gateway
|
||||
# NIC is the corporate LAN.
|
||||
'role': 'Machine link' if (ipv4 and not gateway) else 'LAN',
|
||||
'link': _link_label(entry.get('linkspeedbps'),
|
||||
entry.get('fullduplex')),
|
||||
})
|
||||
return success_response(rows)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user