"""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, "gone")
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",
"Test Host Name",
"",
),
)
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",
"true",
"false172.22.8.61",
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 "8" in request.xml
assert 'ManagedSystem' 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",
"0",
"2",
),
)
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",
"0",
"4096",
),
)
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 "1" 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",
"0",
"2",
),
)
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",
"true",
"false",
)
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 "3" 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()