feat: Intel AMT driver over WS-Management (0.1.0)

An `amt` driver (napalm_amt.AmtDriver), a ManagementControllerDriver for the
management controller of Intel vPro machines, with its own lean WS-Management
client (requests + ElementTree, HTTP Digest, ports 16992/16993).

It reads the machine's identity, hardware with the SMBIOS system UUID, firmware
(AMT with build, BIOS), power state and the hardware event log, reports what AMT
listens on (plain HTTP or TLS, KVM incl. port 5900, IDE-R), and switches the
machine's power. AMT cannot be reset over WS-Management, so there is no
reboot_host. Discovery recognises AMT by its ports and its Server header.

Tested against Intel's published AMT 12 recordings (go-wsman-messages,
Apache-2.0, see NOTICE). `python -m napalm_amt.record` records a real AMT
read-only; the first test devices are two Lenovo ThinkCentre M910x.
This commit is contained in:
christianmanivong committed 2026-10-18 14:55:32 +02:00
commit 0052ba7884
61 files changed
+4303

No files matched your search

View File
Whitespace-only changes.
+100
View File
@@ -0,0 +1,100 @@
"""CPUs, DIMMs and drives of the managed machine, from the recorded NUC9."""
import pytest
from napalm_amt.parsers import parse_cpus, parse_drives, parse_memory
from ..conftest import instances
def test_cpus_take_the_model_from_the_chip():
cpus = parse_cpus(instances("CIM_Processor.pull.xml"), instances("CIM_Chip.pull.xml"))
assert cpus == [
{
"socket": "CPU 0",
"model": "Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz",
"cores": None,
"threads": None,
"status": "OK",
}
]
@pytest.mark.parametrize(
("cpu_status", "status"),
[("1", "OK"), ("4", "OK"), ("2", "Warning"), ("3", "Critical"), ("0", ""), ("", "")],
)
def test_cpu_status_follows_cpustatus(cpu_status, status):
(cpu,) = parse_cpus([{"DeviceID": "CPU 0", "CPUStatus": cpu_status}], [])
assert cpu["status"] == status
assert cpu["model"] == ""
def test_memory():
dimms = parse_memory(instances("CIM_PhysicalMemory.pull.xml"))
assert dimms[0] == {
"slot": "BANK 0",
"size_bytes": 17179869184,
"type": "DDR4",
"speed_mts": 2400,
"part_number": "CT16G4SFD824A.M16FE",
"serial_number": "E0E8D670",
"status": "",
}
assert [d["slot"] for d in dimms] == ["BANK 0", "BANK 2"]
@pytest.mark.parametrize(
("code", "name"),
[("24", "DDR3"), ("26", "DDR4"), ("29", "LPDDR3"), ("34", "DDR5"), ("2", ""), ("", "")],
)
def test_memory_type_follows_smbios(code, name):
(dimm,) = parse_memory([{"BankLabel": "BANK 0", "MemoryType": code}])
assert dimm["type"] == name
def test_memory_without_a_configured_speed_falls_back_to_the_maximum():
(dimm,) = parse_memory(
[{"BankLabel": "A", "ConfiguredMemoryClockSpeed": "0", "MaxMemorySpeed": "2133"}]
)
assert dimm["speed_mts"] == 2133
def test_memory_slot_falls_back_to_the_tag():
(dimm,) = parse_memory([{"Tag": "ChannelA-DIMM0", "Capacity": "8589934592"}])
assert dimm["slot"] == "ChannelA-DIMM0"
assert dimm["size_bytes"] == 8589934592
def test_drives_size_is_in_kilobytes_of_1000():
drives = parse_drives(instances("CIM_MediaAccessDevice.pull.xml"))
# 500107862 KB is the classic 500 GB disk (976773168 sectors of 512 bytes).
assert drives == [
{
"slot": "MEDIA DEV 0",
"model": "",
"serial_number": "",
"size_bytes": 960197124000,
"media": "",
"protocol": "",
"status": "",
},
{
"slot": "MEDIA DEV 1",
"model": "",
"serial_number": "",
"size_bytes": 500107862000,
"media": "",
"protocol": "",
"status": "",
},
]
@pytest.mark.parametrize(
("operational", "status"), [("2", "OK"), ("3", "Warning"), ("6", "Critical"), ("0", "")]
)
def test_drive_status_follows_operational_status(operational, status):
(drive,) = parse_drives([{"DeviceID": "MEDIA DEV 0", "OperationalStatus": operational}])
assert drive["status"] == status
assert drive["size_bytes"] is None
+75
View File
@@ -0,0 +1,75 @@
"""AMT's hardware event log (AMT_MessageLog records), from the recorded NUC9."""
import base64
import pytest
from napalm_amt.parsers import parse_event_record
from ..conftest import instances
def _records():
(out,) = instances("AMT_MessageLog.GetRecords.xml")
return out["RecordArray"]
def test_failed_logins_are_critical():
entry = parse_event_record(_records()[0], "1")
assert entry == {
"id": "1",
"timestamp": 1704511587.0,
"severity": "Critical",
"message": "Intel(r) ME: Authentication failed 10 times. The system may be under attack.",
}
def test_bios_progress_is_ok():
entry = parse_event_record(_records()[1], "2")
assert entry["severity"] == "OK"
assert entry["message"] == "BIOS: PCI resource configuration"
assert entry["timestamp"] == 1694565922.0
def _record(sensor_type, offset, severity, entity, data=(0,) * 8, timestamp=1700000000):
raw = (
timestamp.to_bytes(4, "little")
+ bytes([0xFF, sensor_type, 0x6F, offset, 0x68, severity, 0xFF, entity, 0])
+ bytes(data)
)
return base64.b64encode(raw).decode()
@pytest.mark.parametrize(
("severity", "expected"),
[
(0, "OK"),
(1, "OK"),
(2, "OK"),
(4, "OK"),
(8, "Warning"),
(16, "Critical"),
(32, "Critical"),
],
)
def test_severity(severity, expected):
assert parse_event_record(_record(37, 0, severity, 34), "1")["severity"] == expected
def test_firmware_error():
entry = parse_event_record(_record(15, 0, 16, 34, (0, 1, 0, 0, 0, 0, 0, 0)), "1")
assert entry["message"] == ("BIOS: No system memory is physically installed in the system.")
def test_unknown_sensor_type_is_named():
entry = parse_event_record(_record(200, 0, 2, 2), "1")
assert entry["message"] == "Unknown: sensor type 200"
def test_no_timestamp_is_none():
assert parse_event_record(_record(30, 0, 16, 34, timestamp=0), "1")["timestamp"] is None
def test_a_short_or_broken_record_is_skipped():
assert parse_event_record("AAAA", "1") is None
assert parse_event_record("not base64!", "1") is None
+63
View File
@@ -0,0 +1,63 @@
"""AMT's own network ports (AMT_EthernetPortSettings), from the recorded NUC9."""
from napalm_amt.parsers import parse_ethernet_ports
from ..conftest import instances
def test_the_wired_port_is_kept_and_the_unused_wireless_one_dropped():
# The NUC9 lists a wireless port without a MAC address: there is no WLAN card.
ports = parse_ethernet_ports(instances("AMT_EthernetPortSettings.pull.xml"))
assert ports == [
{
"name": "wired",
"mac_address": "00:be:43:d8:22:a4",
"link_up": True,
"dhcp": True,
"shared_ip": True,
"ip": "",
"prefix_length": None,
}
]
def test_a_static_address_with_its_mask():
(port,) = parse_ethernet_ports(
[
{
"InstanceID": "Intel(r) AMT Ethernet Port Settings 0",
"MACAddress": "6C-4B-90-12-34-56",
"LinkIsUp": "true",
"DHCPEnabled": "false",
"SharedStaticIp": "false",
"IPAddress": "172.22.8.61",
"SubnetMask": "255.255.255.0",
"PhysicalConnectionType": "0",
}
]
)
assert port["mac_address"] == "6c:4b:90:12:34:56"
assert (port["ip"], port["prefix_length"]) == ("172.22.8.61", 24)
assert port["dhcp"] is False
assert port["shared_ip"] is False
def test_a_wireless_port_with_a_card_is_named_wireless():
ports = parse_ethernet_ports(
[
{
"InstanceID": "Intel(r) AMT Ethernet Port Settings 1",
"MACAddress": "a0-88-69-01-02-03",
"PhysicalConnectionType": "3",
"LinkIsUp": "false",
},
]
)
assert [(p["name"], p["link_up"]) for p in ports] == [("wireless", False)]
def test_an_invalid_mask_gives_no_prefix():
(port,) = parse_ethernet_ports(
[{"MACAddress": "00-be-43-d8-22-a4", "IPAddress": "10.0.0.5", "SubnetMask": "255.0.255.0"}]
)
assert port["prefix_length"] is None
+73
View File
@@ -0,0 +1,73 @@
"""The machine's power state and what AMT accepts, from the recorded NUC9."""
import pytest
from napalm_amt.parsers import POWER_REQUESTS, accepted_actions, parse_power_state
from ..conftest import instances
def test_power_state_of_a_running_machine():
(service,) = instances("CIM_AssociatedPowerManagementService.pull.xml")
assert parse_power_state(service) == {
"state": "On",
"available": [10, 8, 5, 11, 4, 7, 14, 12],
}
@pytest.mark.parametrize(
("code", "state"),
[
("2", "On"),
("8", "Off"),
("6", "Off"),
("13", "Off"),
# Asleep or hibernating: the OS is not running, On wakes it.
("3", "Off"),
("4", "Off"),
("7", "Off"),
("99", ""),
("", ""),
],
)
def test_power_state_codes(code, state):
assert parse_power_state({"PowerState": code})["state"] == state
def test_a_single_available_state_is_a_list():
assert parse_power_state({"PowerState": "8", "AvailableRequestedPowerStates": "2"})[
"available"
] == [2]
def test_running_machine_accepts_what_amt_offers_except_on():
assert accepted_actions("On", [10, 8, 5, 11, 4, 7, 14, 12]) == [
"GracefulShutdown",
"ForceOff",
"GracefulRestart",
"ForceRestart",
"PowerCycle",
]
def test_without_graceful_states_amt_offers_no_graceful_actions():
# AMT lists the graceful states only where the host can honour them.
assert accepted_actions("On", [10, 8, 5]) == ["ForceOff", "ForceRestart", "PowerCycle"]
def test_machine_that_is_off_accepts_on_only():
assert accepted_actions("Off", [2]) == ["On"]
assert accepted_actions("Off", []) == ["On"]
def test_unknown_state_accepts_nothing():
assert accepted_actions("", [2, 8, 10]) == []
def test_power_requests_cover_every_role_action():
from napalm_device_types.models import POWER_ACTIONS
assert set(POWER_REQUESTS) == set(POWER_ACTIONS)
assert POWER_REQUESTS["On"] == 2
assert POWER_REQUESTS["ForceOff"] == 8
assert POWER_REQUESTS["ForceRestart"] == 10
+97
View File
@@ -0,0 +1,97 @@
"""What AMT listens on: plain HTTP or TLS, the KVM console and IDE/USB redirection."""
import pytest
from napalm_amt.parsers import parse_management_protocols
from ..conftest import instances
def _protocols(**overrides):
args = {
"tls_settings": instances("AMT_TLSSettingData.pull.xml"),
"redirection": instances("AMT_RedirectionService.get.xml")[0],
"kvm_settings": instances("IPS_KVMRedirectionSettingData.get.xml")[0],
"kvm_saps": instances("CIM_KVMRedirectionSAP.pull.xml"),
}
args.update(overrides)
return parse_management_protocols(**args)
def test_amt_without_tls_listens_on_plain_http():
assert _protocols() == {
"ipmi": {"enabled": False, "port": 623},
"ssh": {"enabled": False, "port": 22},
"http": {"enabled": True, "port": 16992},
"https": {"enabled": False, "port": 16993},
"snmp": {"enabled": False, "port": 161},
# KVM is enabled (MEBx), offline until someone connects; over the redirection
# port, as the standard VNC port 5900 is off.
"kvm": {"enabled": True, "port": 16994},
# IDE-R/USB-R and SOL are both on (EnabledState 32771).
"virtual_media": {"enabled": True, "port": 16994},
}
def test_amt_with_tls_listens_on_16993_and_redirects_over_tls():
tls = [
{
"InstanceID": "Intel(r) AMT 802.3 TLS Settings",
"Enabled": "true",
"AcceptNonSecureConnections": "false",
}
]
protocols = _protocols(tls_settings=tls)
assert protocols["https"] == {"enabled": True, "port": 16993}
assert protocols["http"] == {"enabled": False, "port": 16992}
assert protocols["kvm"]["port"] == 16995
assert protocols["virtual_media"]["port"] == 16995
def test_tls_that_still_accepts_plain_connections_keeps_http_open():
tls = [
{
"InstanceID": "Intel(r) AMT 802.3 TLS Settings",
"Enabled": "true",
"AcceptNonSecureConnections": "true",
}
]
assert _protocols(tls_settings=tls)["http"]["enabled"] is True
def test_the_lms_tls_setting_is_local_and_ignored():
tls = [{"InstanceID": "Intel(r) AMT LMS TLS Settings", "Enabled": "true"}]
assert _protocols(tls_settings=tls)["https"]["enabled"] is False
def test_kvm_on_the_standard_vnc_port():
kvm = {**instances("IPS_KVMRedirectionSettingData.get.xml")[0], "Is5900PortEnabled": "true"}
assert _protocols(kvm_settings=kvm)["kvm"] == {"enabled": True, "port": 5900}
@pytest.mark.parametrize("sap_state", ["3", "4"])
def test_kvm_disabled(sap_state):
saps = [{"EnabledState": sap_state}]
assert _protocols(kvm_saps=saps)["kvm"]["enabled"] is False
def test_kvm_disabled_in_mebx():
kvm = {**instances("IPS_KVMRedirectionSettingData.get.xml")[0], "EnabledByMEBx": "false"}
assert _protocols(kvm_settings=kvm)["kvm"]["enabled"] is False
def test_kvm_unknown_without_any_data():
assert _protocols(kvm_settings={}, kvm_saps=[])["kvm"]["enabled"] is False
@pytest.mark.parametrize(
("state", "enabled"),
[("32768", False), ("32769", True), ("32770", False), ("32771", True), ("", False)],
)
def test_virtual_media_is_ide_redirection(state, enabled):
assert _protocols(redirection={"EnabledState": state})["virtual_media"]["enabled"] is enabled
def test_redirection_listener_off_closes_virtual_media():
redirection = {"EnabledState": "32771", "ListenerEnabled": "false"}
assert _protocols(redirection=redirection)["virtual_media"]["enabled"] is False
+98
View File
@@ -0,0 +1,98 @@
"""The machine's identity and AMT's firmware, from the recorded NUC9 (AMT 12.0.67)."""
import pytest
from napalm_amt.parsers import (
parse_firmware,
parse_platform_guid,
parse_software_identity,
parse_system_identity,
uuid_from_base64,
)
from ..conftest import instances
def test_software_identity_gives_the_amt_version_with_its_build():
identity = parse_software_identity(instances("CIM_SoftwareIdentity.pull.xml"))
assert identity["amt"] == "12.0.67.1579"
assert identity["flash"] == "12.0.67"
def test_software_identity_without_build_keeps_the_version():
identity = parse_software_identity([{"InstanceID": "AMT", "VersionString": "11.8.50"}])
assert identity["amt"] == "11.8.50"
def test_software_identity_of_nothing_is_empty():
assert parse_software_identity([]) == {"amt": "", "flash": ""}
def test_platform_guid_is_the_smbios_uuid():
# AMT sends the SMBIOS UUID's bytes as stored; the first three groups are
# little-endian, as dmidecode and Intel's own console decode them.
(package,) = instances("CIM_ComputerSystemPackage.pull.xml")
assert parse_platform_guid(package["PlatformGUID"]) == "55e3ae13-bfd2-61bb-17a0-88aedd7037ea"
def test_get_uuid_and_platform_guid_agree():
(out,) = instances("AMT_SetupAndConfigurationService.GetUuid.xml")
assert uuid_from_base64(out["UUID"]) == "55e3ae13-bfd2-61bb-17a0-88aedd7037ea"
@pytest.mark.parametrize("guid", ["", "zz", "13AEE355", "0" * 32, "F" * 32])
def test_platform_guid_placeholders_and_garbage_are_empty(guid):
assert parse_platform_guid(guid) == ""
def test_system_identity_comes_from_the_chassis():
identity = parse_system_identity(
instances("CIM_Chassis.pull.xml"), instances("CIM_Card.pull.xml")
)
assert identity == {
"manufacturer": "Intel(R) Client Systems",
"model": "NUC9V7QNX",
"serial_number": "JRQN0243007J",
"board_model": "NUC9V7QNB",
"board_serial": "KNQN0221020W",
}
def test_system_identity_falls_back_to_the_board_for_placeholders():
chassis = [
{
"Manufacturer": "Default string",
"Model": "To Be Filled By O.E.M.",
"SerialNumber": "Default string",
}
]
card = [{"Manufacturer": "LENOVO", "Model": "3102", "SerialNumber": "L1HF8AB01XY"}]
identity = parse_system_identity(chassis, card)
assert identity["manufacturer"] == "LENOVO"
assert identity["model"] == "3102"
assert identity["serial_number"] == "L1HF8AB01XY"
def test_system_identity_of_nothing_is_empty():
assert parse_system_identity([], []) == {
"manufacturer": "",
"model": "",
"serial_number": "",
"board_model": "",
"board_serial": "",
}
def test_firmware_lists_amt_and_the_bios():
firmware = parse_firmware(
parse_software_identity(instances("CIM_SoftwareIdentity.pull.xml")),
instances("CIM_BIOSElement.pull.xml"),
)
assert firmware == [
{"name": "Intel AMT", "version": "12.0.67.1579", "updateable": None},
{"name": "BIOS", "version": "QNCFLX70.0054.2020.0810.2227", "updateable": None},
]
def test_firmware_skips_what_is_unknown():
assert parse_firmware({"amt": "", "flash": ""}, []) == []