feat: SystemIdentityMixin reads a host's SMBIOS identity (4.9.0)
What ties a server to its BMC: the BMC reports the server's serial number and system UUID, the host reads the same values from its DMI tables. Both drivers that read DMI (napalm-linux, napalm-proxmox) had a copy of the shell for it, and neither read the board serial. SystemIdentityMixin.get_system_identity() reads product_uuid, product_serial and board_serial in one framed command and returns them with vendor placeholders empty: a Supermicro board's product serial is 0123456789 on every one of them, others read "To be filled by O.E.M." or "Default string", and many cheap boards share the UUID 03000200-0400-0500-0006-000700080009. is_placeholder_identity() is the same filter for a BMC driver's side. A driver supplies only _run_system_identity_command(command, privileged=); the files are root's.
This commit is contained in:
1 parent
a1da0337e2
commit
fde6d717a8
6 files changed
+247
-1
No files matched your search
@@ -125,6 +125,12 @@ from napalm_device_types.systemd import (
|
||||
parse_systemd_services,
|
||||
)
|
||||
from napalm_device_types.updates import UpdateMixin
|
||||
from napalm_device_types.system_identity import (
|
||||
SYSTEM_IDENTITY_COMMAND,
|
||||
SystemIdentityMixin,
|
||||
is_placeholder_identity,
|
||||
parse_system_identity,
|
||||
)
|
||||
from napalm_device_types.virtual_ips import (
|
||||
VIRTUAL_IPS_COMMAND,
|
||||
VirtualIpsMixin,
|
||||
@@ -205,6 +211,10 @@ __all__ = [
|
||||
"UpdateMixin",
|
||||
"VIRTUAL_IPS_COMMAND",
|
||||
"VirtualIpsMixin",
|
||||
"SystemIdentityMixin",
|
||||
"SYSTEM_IDENTITY_COMMAND",
|
||||
"is_placeholder_identity",
|
||||
"parse_system_identity",
|
||||
"OpenSshServerMixin",
|
||||
"SSH_ALGORITHM_CLASSES",
|
||||
"SshHardeningPlan",
|
||||
|
||||
@@ -1151,3 +1151,13 @@ class ManagementProtocolsDict(TypedDict):
|
||||
snmp: ProtocolStateDict
|
||||
kvm: ProtocolStateDict
|
||||
virtual_media: ProtocolStateDict
|
||||
|
||||
|
||||
class SystemIdentityDict(TypedDict):
|
||||
"""A host's SMBIOS identity, what ties it to its BMC (:mod:`napalm_device_types.system_identity`).
|
||||
|
||||
Each value is empty when unknown or a vendor placeholder."""
|
||||
|
||||
system_uuid: str # lowercase
|
||||
product_serial: str
|
||||
board_serial: str
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The SMBIOS identity of a host: system UUID, product serial, board serial.
|
||||
|
||||
What ties a server to its BMC. The BMC reports the server's serial number and
|
||||
system UUID; the host reads the same values from its firmware tables. Both sides
|
||||
read them the same way on every Linux host, so the command, its parse and the
|
||||
placeholder filter live here once, and a driver only carries the command across.
|
||||
|
||||
**Placeholders are no identity.** Board vendors leave defaults in the DMI tables
|
||||
that are the same on thousands of machines: a Supermicro board's product serial
|
||||
is ``0123456789``, others read ``To be filled by O.E.M.`` or ``Default string``,
|
||||
and a common UUID on cheap boards is ``03000200-0400-0500-0006-000700080009``.
|
||||
Matching on those would join unrelated machines, so they read as empty.
|
||||
|
||||
**Root.** ``product_uuid`` and ``product_serial`` are readable by root only; without
|
||||
it they come back empty, which is "not known", not a mismatch.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from napalm_device_types.models import SystemIdentityDict
|
||||
from napalm_device_types.terminal import strip_terminal_codes
|
||||
|
||||
_BEGIN = "IDBEGIN"
|
||||
_END = "IDEND"
|
||||
_DMI = "/sys/class/dmi/id"
|
||||
|
||||
#: Reads the three files in one round trip, framed so a banner or prompt around the
|
||||
#: output cannot shift a field.
|
||||
SYSTEM_IDENTITY_COMMAND = (
|
||||
f"printf '{_BEGIN}\\n%s\\n%s\\n%s\\n{_END}\\n' "
|
||||
f"\"$(cat {_DMI}/product_uuid 2>/dev/null)\" "
|
||||
f"\"$(cat {_DMI}/product_serial 2>/dev/null)\" "
|
||||
f"\"$(cat {_DMI}/board_serial 2>/dev/null)\""
|
||||
)
|
||||
|
||||
#: Lowercased defaults board vendors leave in place.
|
||||
_PLACEHOLDERS = frozenset({
|
||||
"", "none", "n/a", "na", "not specified", "not applicable", "not available",
|
||||
"to be filled by o.e.m.", "to be filled by oem", "default string", "default",
|
||||
"system serial number", "base board serial number", "chassis serial number",
|
||||
"serial number", "0123456789", "1234567890", "123456789", "o.e.m.", "oem",
|
||||
"03000200-0400-0500-0006-000700080009",
|
||||
})
|
||||
#: A value of one repeated character ("0", "00000000", "FFFF-...") or of zeros and Fs.
|
||||
_REPEATED = re.compile(r"^(.)\1*$")
|
||||
|
||||
|
||||
def is_placeholder_identity(value: str | None) -> bool:
|
||||
"""Whether *value* is a vendor default rather than this machine's identity."""
|
||||
text = (value or "").strip().lower()
|
||||
if text in _PLACEHOLDERS:
|
||||
return True
|
||||
compact = text.replace("-", "").replace(" ", "")
|
||||
return bool(_REPEATED.match(compact))
|
||||
|
||||
|
||||
def _clean(value: str) -> str:
|
||||
value = value.strip()
|
||||
return "" if is_placeholder_identity(value) else value
|
||||
|
||||
|
||||
def parse_system_identity(output: str) -> SystemIdentityDict:
|
||||
"""The identity from :data:`SYSTEM_IDENTITY_COMMAND`'s output, placeholders empty.
|
||||
|
||||
:raises ValueError: when the output carries no intact frame.
|
||||
"""
|
||||
lines = strip_terminal_codes(output).splitlines()
|
||||
try:
|
||||
start = lines.index(_BEGIN) + 1
|
||||
end = lines.index(_END, start)
|
||||
except ValueError:
|
||||
raise ValueError("system identity: no intact IDBEGIN/IDEND frame") from None
|
||||
fields = (lines[start:end] + ["", "", ""])[:3]
|
||||
uuid, product_serial, board_serial = (_clean(f) for f in fields)
|
||||
return {
|
||||
"system_uuid": uuid.lower(),
|
||||
"product_serial": product_serial,
|
||||
"board_serial": board_serial,
|
||||
}
|
||||
|
||||
|
||||
class SystemIdentityMixin:
|
||||
"""Adds :meth:`get_system_identity` to a driver that can run a command on a Linux host.
|
||||
|
||||
The template form (README, "Function classes"): command and parse are the same
|
||||
everywhere and concrete here; a driver supplies only
|
||||
:meth:`_run_system_identity_command` -- how a command reaches its host, and how it
|
||||
gains root there. Mixed in by the drivers that can, so
|
||||
``hasattr(driver, "get_system_identity")`` stays a truthful answer.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - declared for type checkers only
|
||||
|
||||
def _run_system_identity_command(self, command: str, *, privileged: bool) -> str:
|
||||
"""Run *command* on the host and return what it printed; as root when
|
||||
*privileged* (``product_uuid`` and ``product_serial`` are root's)."""
|
||||
...
|
||||
|
||||
def get_system_identity(self) -> SystemIdentityDict:
|
||||
"""
|
||||
Returns the host's SMBIOS identity, each value empty when unknown or a vendor
|
||||
placeholder:
|
||||
|
||||
* system_uuid (str) - lowercase
|
||||
* product_serial (str)
|
||||
* board_serial (str)
|
||||
|
||||
:raises ValueError: if the output carried no intact reading.
|
||||
"""
|
||||
output = self._run_system_identity_command(SYSTEM_IDENTITY_COMMAND, privileged=True)
|
||||
return parse_system_identity(output)
|
||||
Reference in new issue
Block a user