"""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, "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()