SNMP_CAPABLE = False, so netOrk does not poll AMT over SNMP the way it reads other management controllers (UCD-MIB).
334 lines
10 KiB
Python
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()
|