--- /dev/null
+full_set_with_critical = {
+ "host": "host01",
+ "sn": "12345",
+ "status": {
+ "storage": {
+ "disk.bay.0:enclosure.internal.0-1:raid.integrated.1-1": {
+ "description": "Solid State Disk 0:1:0",
+ "entity": "RAID.Integrated.1-1",
+ "capacity_bytes": 959656755200,
+ "model": "KPM5XVUG960G",
+ "protocol": "SAS",
+ "serial_number": "8080A1CRTP5F",
+ "status": {
+ "health": "Critical",
+ "healthrollup": "OK",
+ "state": "Enabled"
+ },
+ "physical_location": {
+ "partlocation": {
+ "locationordinalvalue": 0,
+ "locationtype": "Slot"
+ }
+ }
+ },
+ "disk.bay.9:enclosure.internal.0-1": {
+ "description": "PCIe SSD in Slot 9 in Bay 1",
+ "entity": "CPU.1",
+ "capacity_bytes": 1600321314816,
+ "model": "Dell Express Flash NVMe P4610 1.6TB SFF",
+ "protocol": "PCIe",
+ "serial_number": "PHLN035305MN1P6AGN",
+ "status": {
+ "health": "Critical",
+ "healthrollup": "OK",
+ "state": "Enabled"
+ },
+ "physical_location": {
+ "partlocation": {
+ "locationordinalvalue": 9,
+ "locationtype": "Slot"
+ }
+ }
+ }
+ },
+ "processors": {
+ "cpu.socket.2": {
+ "description": "Represents the properties of a Processor attached to this System",
+ "total_cores": 20,
+ "total_threads": 40,
+ "processor_type": "CPU",
+ "model": "Intel(R) Xeon(R) Gold 6230 CPU @ 2.10GHz",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ },
+ "manufacturer": "Intel"
+ },
+
+ },
+ "network": {
+ "nic.slot.1-1-1": {
+ "description": "NIC in Slot 1 Port 1 Partition 1",
+ "name": "System Ethernet Interface",
+ "speed_mbps": 0,
+ "status": {
+ "health": "OK",
+ "state": "StandbyOffline"
+ }
+ }
+ },
+ "memory": {
+ "dimm.socket.a1": {
+ "description": "DIMM A1",
+ "memory_device_type": "DDR4",
+ "capacity_mi_b": 31237,
+ "status": {
+ "health": "Critical",
+ "state": "Enabled"
+ }
+ }
+ }
+ },
+ "firmwares": {
+
+ }
+}
+
+mgr_inventory_cache = {"host01": {"hostname": "host01",
+ "addr": "10.10.10.11",
+ "labels": ["_admin"],
+ "status": "",
+ "idrac": {"hostname": "10.10.10.11",
+ "username": "root",
+ "password": "ceph123"}},
+ "host02": {"hostname": "host02",
+ "addr": "10.10.10.12",
+ "labels": [],
+ "status": "",
+ "idrac": {"hostname": "10.10.10.12",
+ "username": "root",
+ "password": "ceph123"}}}
+
+full_set = {
+ "host01": {
+ "host": "host01",
+ "sn": "FR8Y5X3",
+ "status": {
+ "storage": {
+ "disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1": {
+ "description": "Disk 8 in Backplane 1 of Storage Controller in Slot 2",
+ "entity": "NonRAID.Slot.2-1",
+ "capacity_bytes": 20000588955136,
+ "model": "ST20000NM008D-3D",
+ "protocol": "SATA",
+ "serial_number": "ZVT99QLL",
+ "status": {
+ "health": "OK",
+ "healthrollup": "OK",
+ "state": "Enabled"
+ },
+ "physical_location": {
+ "partlocation": {
+ "locationordinalvalue": 8,
+ "locationtype": "Slot"
+ }
+ }
+ }
+ },
+ "processors": {
+ "cpu.socket.2": {
+ "description": "Represents the properties of a Processor attached to this System",
+ "total_cores": 16,
+ "total_threads": 32,
+ "processor_type": "CPU",
+ "model": "Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ },
+ "manufacturer": "Intel"
+ },
+ "cpu.socket.1": {
+ "description": "Represents the properties of a Processor attached to this System",
+ "total_cores": 16,
+ "total_threads": 32,
+ "processor_type": "CPU",
+ "model": "Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ },
+ "manufacturer": "Intel"
+ }
+ },
+ "network": {
+ "oslogicalnetwork.2": {
+ "description": "eno8303",
+ "name": "eno8303",
+ "speed_mbps": 0,
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ },
+ "memory": {
+ "dimm.socket.a1": {
+ "description": "DIMM A1",
+ "memory_device_type": "DDR4",
+ "capacity_mi_b": 16384,
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ },
+ "power": {
+ "0": {
+ "name": "PS1 Status",
+ "model": "PWR SPLY,800W,RDNT,LTON",
+ "manufacturer": "DELL",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ },
+ "1": {
+ "name": "PS2 Status",
+ "model": "PWR SPLY,800W,RDNT,LTON",
+ "manufacturer": "DELL",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ },
+ "fans": {
+ "0": {
+ "name": "System Board Fan1A",
+ "physical_context": "SystemBoard",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ }
+ },
+ "firmwares": {
+ "installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1": {
+ "name": "Lifecycle Controller",
+ "description": "Represents Firmware Inventory",
+ "release_date": "00:00:00Z",
+ "version": "6.10.30.20",
+ "updateable": True,
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ }
+ },
+"host02": {
+ "host": "host02",
+ "sn": "FR8Y5X4",
+ "status": {
+ "storage": {
+ "disk.bay.8:enclosure.internal.0-1:nonraid.slot.2-1": {
+ "description": "Disk 8 in Backplane 1 of Storage Controller in Slot 2",
+ "entity": "NonRAID.Slot.2-1",
+ "capacity_bytes": 20000588955136,
+ "model": "ST20000NM008D-3D",
+ "protocol": "SATA",
+ "serial_number": "ZVT99QLL",
+ "status": {
+ "health": "OK",
+ "healthrollup": "OK",
+ "state": "Enabled"
+ },
+ "physical_location": {
+ "partlocation": {
+ "locationordinalvalue": 8,
+ "locationtype": "Slot"
+ }
+ }
+ }
+ },
+ "processors": {
+ "cpu.socket.2": {
+ "description": "Represents the properties of a Processor attached to this System",
+ "total_cores": 16,
+ "total_threads": 32,
+ "processor_type": "CPU",
+ "model": "Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ },
+ "manufacturer": "Intel"
+ },
+ "cpu.socket.1": {
+ "description": "Represents the properties of a Processor attached to this System",
+ "total_cores": 16,
+ "total_threads": 32,
+ "processor_type": "CPU",
+ "model": "Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ },
+ "manufacturer": "Intel"
+ }
+ },
+ "network": {
+ "oslogicalnetwork.2": {
+ "description": "eno8303",
+ "name": "eno8303",
+ "speed_mbps": 0,
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ },
+ "memory": {
+ "dimm.socket.a1": {
+ "description": "DIMM A1",
+ "memory_device_type": "DDR4",
+ "capacity_mi_b": 16384,
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ },
+ "power": {
+ "0": {
+ "name": "PS1 Status",
+ "model": "PWR SPLY,800W,RDNT,LTON",
+ "manufacturer": "DELL",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ },
+ "1": {
+ "name": "PS2 Status",
+ "model": "PWR SPLY,800W,RDNT,LTON",
+ "manufacturer": "DELL",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ },
+ "fans": {
+ "0": {
+ "name": "System Board Fan1A",
+ "physical_context": "SystemBoard",
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ }
+ },
+ "firmwares": {
+ "installed-28897-6.10.30.20__usc.embedded.1:lc.embedded.1": {
+ "name": "Lifecycle Controller",
+ "description": "Represents Firmware Inventory",
+ "release_date": "00:00:00Z",
+ "version": "6.10.30.20",
+ "updateable": True,
+ "status": {
+ "health": "OK",
+ "state": "Enabled"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+import pytest
+import cherrypy
+import json
+from _pytest.monkeypatch import MonkeyPatch
+from cherrypy.test import helper
+from cephadm.agent import NodeProxy
+from unittest.mock import MagicMock, call
+from cephadm.http_server import CephadmHttpServer
+from cephadm.inventory import AgentCache, NodeProxyCache, Inventory
+from cephadm.ssl_cert_utils import SSLCerts
+from urllib.error import URLError
+from . import node_proxy_data
+
+PORT = 58585
+
+
+class FakeMgr:
+ def __init__(self) -> None:
+ self.log = MagicMock()
+ self.get_store = MagicMock(return_value=json.dumps(node_proxy_data.mgr_inventory_cache))
+ self.set_store = MagicMock()
+ self.set_health_warning = MagicMock()
+ self.remove_health_warning = MagicMock()
+ self.inventory = Inventory(self)
+ self.agent_cache = AgentCache(self)
+ self.node_proxy = NodeProxyCache(self)
+ self.node_proxy.save = MagicMock()
+ self.http_server = MagicMock()
+ self.http_server.agent = MagicMock()
+ self.http_server.agent.ssl_certs = SSLCerts()
+ self.http_server.agent.ssl_certs.generate_root_cert(self.get_mgr_ip())
+
+ def get_mgr_ip(self) -> str:
+ return '0.0.0.0'
+
+class TestNodeProxy(helper.CPWebCase):
+ mgr = FakeMgr()
+ app = NodeProxy(mgr)
+ mgr.agent_cache.agent_keys = {"host01": "fake-secret01",
+ "host02": "fake-secret02"}
+ mgr.node_proxy.idrac = {"host01": {"username": "idrac-user01",
+ "password": "idrac-pass01"},
+ "host02": {"username": "idrac-user02",
+ "password": "idrac-pass02"}}
+ mgr.node_proxy.data = node_proxy_data.full_set
+
+ @classmethod
+ def setup_server(cls):
+ # cherrypy.tree.mount(NodeProxy(TestNodeProxy.mgr))
+ cherrypy.tree.mount(TestNodeProxy.app)
+ cherrypy.config.update({'global': {
+ 'server.socket_host': '127.0.0.1',
+ 'server.socket_port': PORT}})
+
+ def setUp(self):
+ self.PORT = PORT
+ self.monkeypatch = MonkeyPatch()
+
+ def test_idrac_data_misses_cephx_field(self):
+ data = '{}'
+ self.getPage("/idrac", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('400 Bad Request')
+
+ def test_idrac_data_misses_name_field(self):
+ data = '{"cephx": {"secret": "fake-secret"}}'
+ self.getPage("/idrac", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('400 Bad Request')
+
+ def test_idrac_data_misses_secret_field(self):
+ data = '{"cephx": {"name": "host01"}}'
+ self.getPage("/idrac", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('400 Bad Request')
+
+ def test_idrac_agent_not_running(self):
+ data = '{"cephx": {"name": "host03", "secret": "fake-secret03"}}'
+ self.getPage("/idrac", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('502 Bad Gateway')
+
+ def test_idrac_wrong_keyring(self):
+ data = '{"cephx": {"name": "host01", "secret": "wrong-keyring"}}'
+ self.getPage("/idrac", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('403 Forbidden')
+
+ def test_idrac_ok(self):
+ data = '{"cephx": {"name": "host01", "secret": "fake-secret01"}}'
+ self.getPage("/idrac", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('200 OK')
+
+ def test_data_missing_patch(self):
+ data = '{"cephx": {"name": "host01", "secret": "fake-secret01"}}'
+ self.getPage("/data", method="POST", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('400 Bad Request')
+
+ def test_data_raises_alert(self):
+ patch = node_proxy_data.full_set_with_critical
+ data = {"cephx": {"name": "host01", "secret": "fake-secret01"}, "patch": patch}
+ data_str = json.dumps(data)
+ self.getPage("/data", method="POST", body=data_str, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data_str)))])
+ self.assertStatus('200 OK')
+
+ calls = [call('HARDWARE_STORAGE',
+ count=2,
+ detail=['disk.bay.0:enclosure.internal.0-1:raid.integrated.1-1 is critical: Enabled',
+ 'disk.bay.9:enclosure.internal.0-1 is critical: Enabled'],
+ summary='2 storage members are not ok'),
+ call('HARDWARE_MEMORY',
+ count=1,
+ detail=['dimm.socket.a1 is critical: Enabled'],
+ summary='1 memory member is not ok')]
+
+ assert TestNodeProxy.mgr.set_health_warning.mock_calls == calls
+
+ # @pytest.mark.parametrize("method", ["GET", "PATCH"])
+ # def test_led_no_hostname(self, method):
+ # self.getPage("/led", method=method)
+ # self.assertStatus('501 Not Implemented')
+
+ def test_led_GET_no_hostname(self):
+ self.getPage("/led", method="GET")
+ self.assertStatus('501 Not Implemented')
+
+ def test_led_PATCH_no_hostname(self):
+ data = "{}"
+ self.getPage("/led", method="PATCH", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+ self.assertStatus('501 Not Implemented')
+
+ def test_set_led(self):
+ data = '{"state": "on"}'
+ TestNodeProxy.app.query_endpoint = MagicMock(return_value=(200, "OK"))
+ # self.monkeypatch.setattr(NodeProxy, "query_endpoint", lambda *a, **kw: (200, "OK"))
+ self.getPage("/host01/led", method="PATCH", body=data, headers=[('Content-Type', 'application/json'),
+ ('Content-Length', str(len(data)))])
+
+ calls = [call(addr='10.10.10.11',
+ data='{"state": "on"}',
+ endpoint='/led',
+ headers={'Authorization': 'Basic aWRyYWMtdXNlcjAxOmlkcmFjLXBhc3MwMQ=='},
+ method='PATCH',
+ port=8080,
+ ssl_ctx=TestNodeProxy.app.ssl_ctx)]
+ self.assertStatus('200 OK')
+ assert TestNodeProxy.app.query_endpoint.mock_calls == calls
+
+ def test_get_led(self):
+ TestNodeProxy.app.query_endpoint = MagicMock(return_value=(200, "OK"))
+ self.getPage("/host01/led", method="GET")
+ calls = [call(addr='10.10.10.11',
+ data=None,
+ endpoint='/led',
+ headers={},
+ method='GET',
+ port=8080,
+ ssl_ctx=TestNodeProxy.app.ssl_ctx)]
+ self.assertStatus('200 OK')
+ assert TestNodeProxy.app.query_endpoint.mock_calls == calls
+
+ def test_led_endpoint_unreachable(self):
+ TestNodeProxy.app.query_endpoint = MagicMock(side_effect=URLError("fake-error"))
+ self.getPage("/host02/led", method="GET")
+ calls = [call(addr='10.10.10.12',
+ data=None,
+ endpoint='/led',
+ headers={},
+ method='GET',
+ port=8080,
+ ssl_ctx=TestNodeProxy.app.ssl_ctx)]
+ self.assertStatus('502 Bad Gateway')
+ assert TestNodeProxy.app.query_endpoint.mock_calls == calls
+
+ def test_fullreport_with_valid_hostname(self):
+ self.getPage("/host02/fullreport", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_fullreport_no_hostname(self):
+ self.getPage("/fullreport", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_fullreport_with_invalid_hostname(self):
+ self.getPage("/host03/fullreport", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_summary_with_valid_hostname(self):
+ self.getPage("/host02/summary", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_summary_no_hostname(self):
+ self.getPage("/summary", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_summary_with_invalid_hostname(self):
+ self.getPage("/host03/summary", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_criticals_with_valid_hostname(self):
+ self.getPage("/host02/criticals", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_criticals_no_hostname(self):
+ self.getPage("/criticals", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_criticals_with_invalid_hostname(self):
+ self.getPage("/host03/criticals", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_memory_with_valid_hostname(self):
+ self.getPage("/host02/memory", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_memory_no_hostname(self):
+ self.getPage("/memory", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_memory_with_invalid_hostname(self):
+ self.getPage("/host03/memory", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_network_with_valid_hostname(self):
+ self.getPage("/host02/network", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_network_no_hostname(self):
+ self.getPage("/network", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_network_with_invalid_hostname(self):
+ self.getPage("/host03/network", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_processors_with_valid_hostname(self):
+ self.getPage("/host02/processors", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_processors_no_hostname(self):
+ self.getPage("/processors", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_processors_with_invalid_hostname(self):
+ self.getPage("/host03/processors", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_storage_with_valid_hostname(self):
+ self.getPage("/host02/storage", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_storage_no_hostname(self):
+ self.getPage("/storage", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_storage_with_invalid_hostname(self):
+ self.getPage("/host03/storage", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_power_with_valid_hostname(self):
+ self.getPage("/host02/power", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_power_no_hostname(self):
+ self.getPage("/power", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_power_with_invalid_hostname(self):
+ self.getPage("/host03/power", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_fans_with_valid_hostname(self):
+ self.getPage("/host02/fans", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_fans_no_hostname(self):
+ self.getPage("/fans", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_fans_with_invalid_hostname(self):
+ self.getPage("/host03/fans", method="GET")
+ self.assertStatus('404 Not Found')
+
+ def test_firmwares_with_valid_hostname(self):
+ self.getPage("/host02/firmwares", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_firmwares_no_hostname(self):
+ self.getPage("/firmwares", method="GET")
+ self.assertStatus('200 OK')
+
+ def test_firmwares_with_invalid_hostname(self):
+ self.getPage("/host03/firmwares", method="GET")
+ self.assertStatus('404 Not Found')
\ No newline at end of file