Files
christianmanivong 1ef2034d87 fix: AMT runs no SNMP agent (0.1.1)
SNMP_CAPABLE = False, so netOrk does not poll AMT over SNMP the way it reads
other management controllers (UCD-MIB).
2026-10-18 15:45:44 +02:00

334 lines
10 KiB
Python

"""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_has_no_snmp():
# AMT runs no SNMP agent; netOrk must not poll it over SNMP (UCD-MIB) for being
# a management controller.
assert AmtDriver.SNMP_CAPABLE is False
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()