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

+327
View File
@@ -0,0 +1,327 @@
"""The driver end to end against the recorded NUC9 (AMT 12.0.67), served over mocked HTTP."""
import pytest
import requests
import responses
from napalm.base.exceptions import ConnectionException
from napalm_device_types import ManagementControllerDriver
from napalm_amt import AmtDriver
from .conftest import HOST, URL, load, patched
@pytest.fixture
def driver(amt):
d = AmtDriver(HOST, "admin", "P@ssw0rd")
d.open()
yield d
d.close()
def test_is_a_management_controller():
assert issubclass(AmtDriver, ManagementControllerDriver)
assert AmtDriver.ROLE == "management_controller"
assert AmtDriver.DRIVER_NAME == "amt"
assert AmtDriver.default_port == 16992
def test_does_not_offer_a_controller_reset():
# AMT cannot be restarted over WS-Management; the role method stays absent so
# netOrk does not offer it.
assert not hasattr(AmtDriver, "reboot_host")
def test_tls_follows_the_port():
assert AmtDriver(HOST, "u", "p").transport.url == f"http://{HOST}:16992/wsman"
tls = AmtDriver(HOST, "u", "p", optional_args={"port": 16993, "verify_ssl": False})
assert tls.transport.url == f"https://{HOST}:16993/wsman"
assert tls.transport.session.verify is False
# --- connection ------------------------------------------------------------------------
def test_open_reads_the_general_settings(amt):
d = AmtDriver(HOST, "admin", "P@ssw0rd")
d.open()
assert [r.resource for r in amt.requests] == ["AMT_GeneralSettings"]
assert d.is_alive() == {"is_alive": True}
def test_open_with_wrong_credentials_fails():
with responses.RequestsMock() as rsps:
rsps.add(
responses.POST,
URL,
status=401,
headers={"WWW-Authenticate": 'Digest realm="Digest:0", nonce="n", qop="auth"'},
)
with pytest.raises(ConnectionException, match="credentials"):
AmtDriver(HOST, "admin", "wrong", optional_args={"retries": 0}).open()
def test_open_without_an_answer_fails():
with responses.RequestsMock() as rsps:
rsps.add(responses.POST, URL, body=requests.ConnectionError("refused"))
with pytest.raises(ConnectionException):
AmtDriver(HOST, "admin", "pw", optional_args={"retries": 0}).open()
def test_is_alive_asks_amt(driver, amt):
amt.override("AMT_GeneralSettings", "Get", 500, "<html>gone</html>")
assert driver.is_alive() == {"is_alive": False}
def test_is_alive_before_open_is_false():
assert AmtDriver(HOST, "u", "p").is_alive() == {"is_alive": False}
# --- NAPALM getters --------------------------------------------------------------------
def test_facts(driver):
assert driver.get_facts() == {
"uptime": -1.0,
"vendor": "Intel(R) Client Systems",
"model": "NUC9V7QNX",
"hostname": "Test Host Name",
"fqdn": "Test Host Name.Test Domain Name",
"os_version": "12.0.67.1579",
"serial_number": "JRQN0243007J",
"interface_list": ["wired"],
}
def test_facts_without_an_amt_hostname_use_the_address(amt):
amt.override(
"AMT_GeneralSettings",
"Get",
200,
patched(
"AMT_GeneralSettings.get.xml",
"<g:HostName>Test Host Name</g:HostName>",
"<g:HostName></g:HostName>",
),
)
d = AmtDriver(HOST, "admin", "pw")
d.open()
facts = d.get_facts()
assert facts["hostname"] == HOST
assert facts["fqdn"] == HOST
def test_interfaces(driver):
assert driver.get_interfaces() == {
"wired": {
"is_up": True,
"is_enabled": True,
"description": "Intel AMT, wired, DHCP, shares the host's IP address",
"last_flapped": -1.0,
"speed": -1.0,
"mtu": -1,
"mac_address": "00:be:43:d8:22:a4",
}
}
def test_interfaces_ip_without_an_address_is_empty(driver):
# In DHCP mode the NUC9 reports no address of its own.
assert driver.get_interfaces_ip() == {}
def test_interfaces_ip_with_a_static_address(amt):
pull = patched(
"AMT_EthernetPortSettings.pull.xml",
"<h:DHCPEnabled>true</h:DHCPEnabled>",
"<h:DHCPEnabled>false</h:DHCPEnabled><h:IPAddress>172.22.8.61</h:IPAddress>",
1,
)
amt.override("AMT_EthernetPortSettings", "Pull", 200, pull)
d = AmtDriver(HOST, "admin", "pw")
d.open()
assert d.get_interfaces_ip() == {"wired": {"ipv4": {"172.22.8.61": {"prefix_length": 24}}}}
assert "static" in d.get_interfaces()["wired"]["description"]
def test_environment_is_empty(driver):
# AMT on a client platform reports no temperatures, fans or power supplies.
assert driver.get_environment() == {
"fans": {},
"temperature": {},
"power": {},
"cpu": {},
"memory": {},
}
# --- role methods ----------------------------------------------------------------------
def test_power_state(driver):
assert driver.get_power_state() == {
"state": "On",
"actions": [
"GracefulShutdown",
"ForceOff",
"GracefulRestart",
"ForceRestart",
"PowerCycle",
],
}
def test_set_power_state_asks_amt_for_the_cim_state(driver, amt):
driver.set_power_state("ForceOff")
(request,) = amt.calls("RequestPowerStateChange")
assert "<h:PowerState>8</h:PowerState>" in request.xml
assert '<Selector Name="Name">ManagedSystem</Selector>' in request.xml
def test_set_power_state_refuses_an_unknown_action(driver):
with pytest.raises(ValueError):
driver.set_power_state("Explode")
def test_set_power_state_refuses_on_for_a_running_machine(driver, amt):
with pytest.raises(RuntimeError, match="not accepted"):
driver.set_power_state("On")
assert amt.calls("RequestPowerStateChange") == []
def test_set_power_state_reports_amt_refusing(driver, amt):
amt.override(
"CIM_PowerManagementService",
"RequestPowerStateChange",
200,
patched(
"CIM_PowerManagementService.RequestPowerStateChange.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>2</g:ReturnValue>",
),
)
with pytest.raises(RuntimeError, match="refused.*2"):
driver.set_power_state("ForceRestart")
def test_set_power_state_accepts_a_started_job(driver, amt):
amt.override(
"CIM_PowerManagementService",
"RequestPowerStateChange",
200,
patched(
"CIM_PowerManagementService.RequestPowerStateChange.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>4096</g:ReturnValue>",
),
)
driver.set_power_state("PowerCycle")
def test_hardware_inventory(driver):
inventory = driver.get_hardware_inventory()
assert {k: v for k, v in inventory.items() if not isinstance(v, list)} == {
"manufacturer": "Intel(R) Client Systems",
"model": "NUC9V7QNX",
"serial_number": "JRQN0243007J",
"system_uuid": "55e3ae13-bfd2-61bb-17a0-88aedd7037ea",
}
assert [c["model"] for c in inventory["cpus"]] == ["Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz"]
assert len(inventory["memory"]) == 2
assert len(inventory["drives"]) == 2
assert inventory["power_supplies"] == []
assert inventory["nics"] == [
{"name": "wired", "mac_address": "00:be:43:d8:22:a4", "speed_mbps": None, "link_up": True}
]
def test_hardware_inventory_survives_a_class_amt_lacks(driver, amt):
# Older AMT versions know fewer CIM classes; a missing one leaves its part empty.
amt.override(
"CIM_MediaAccessDevice", "Enumerate", 400, load("fault_destination_unreachable.xml")
)
inventory = driver.get_hardware_inventory()
assert inventory["drives"] == []
assert len(inventory["memory"]) == 2
def test_event_log_newest_first(driver, amt):
log = driver.get_event_log()
assert [e["id"] for e in log] == ["1", "2", "3"]
assert log[0]["severity"] == "Critical"
assert log[0]["timestamp"] > log[1]["timestamp"]
(position,) = amt.calls("PositionToFirstRecord")
(records,) = amt.calls("GetRecords")
assert "<h:IterationIdentifier>1</h:IterationIdentifier>" in records.xml
def test_event_log_limit(driver):
assert len(driver.get_event_log(limit=1)) == 1
def test_event_log_empty(driver, amt):
amt.override(
"AMT_MessageLog",
"PositionToFirstRecord",
200,
patched(
"AMT_MessageLog.PositionToFirstRecord.xml",
"<g:ReturnValue>0</g:ReturnValue>",
"<g:ReturnValue>2</g:ReturnValue>",
),
)
assert driver.get_event_log() == []
assert amt.calls("GetRecords") == []
def test_event_log_reads_on_until_no_more_records(driver, amt):
first_page = patched(
"AMT_MessageLog.GetRecords.xml",
"<g:NoMoreRecords>true</g:NoMoreRecords>",
"<g:NoMoreRecords>false</g:NoMoreRecords>",
)
pages = iter([first_page, load("AMT_MessageLog.GetRecords.xml")])
original = amt._answer
def answer(req):
if req.operation == "GetRecords":
return 200, next(pages)
return original(req)
amt._answer = answer
log = driver.get_event_log()
assert len(log) == 6
second = amt.calls("GetRecords")[1]
assert "<h:IterationIdentifier>3</h:IterationIdentifier>" in second.xml
def test_firmware_inventory(driver):
assert driver.get_firmware_inventory() == [
{"name": "Intel AMT", "version": "12.0.67.1579", "updateable": None},
{"name": "BIOS", "version": "QNCFLX70.0054.2020.0810.2227", "updateable": None},
]
def test_management_protocols(driver):
protocols = driver.get_management_protocols()
assert protocols["http"] == {"enabled": True, "port": 16992}
assert protocols["https"] == {"enabled": False, "port": 16993}
assert protocols["kvm"] == {"enabled": True, "port": 16994}
def test_reads_are_cached_per_connection(driver, amt):
driver.get_facts()
driver.get_hardware_inventory()
before = len(amt.requests)
driver.get_facts()
assert len(amt.requests) == before
driver.close()
driver.open()
driver.get_facts()
assert len(amt.requests) > before
def test_getters_before_open_raise():
with pytest.raises(ConnectionException):
AmtDriver(HOST, "u", "p").get_facts()