]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
node-proxy: add unit tests for node-proxy endpoint
authorGuillaume Abrioux <gabrioux@ibm.com>
Thu, 22 Jun 2023 13:54:55 +0000 (15:54 +0200)
committerGuillaume Abrioux <gabrioux@ibm.com>
Thu, 25 Jan 2024 14:53:07 +0000 (14:53 +0000)
This adds some unit tests for the node-proxy endpoint recently added to
the mgr.

Signed-off-by: Guillaume Abrioux <gabrioux@ibm.com>
(cherry picked from commit 300c99a2f0afd5999938e7e614188b80ee61853b)

src/pybind/mgr/cephadm/tests/node_proxy_data.py [new file with mode: 0644]
src/pybind/mgr/cephadm/tests/test_node_proxy.py [new file with mode: 0644]

diff --git a/src/pybind/mgr/cephadm/tests/node_proxy_data.py b/src/pybind/mgr/cephadm/tests/node_proxy_data.py
new file mode 100644 (file)
index 0000000..1073bed
--- /dev/null
@@ -0,0 +1,340 @@
+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
diff --git a/src/pybind/mgr/cephadm/tests/test_node_proxy.py b/src/pybind/mgr/cephadm/tests/test_node_proxy.py
new file mode 100644 (file)
index 0000000..42ab391
--- /dev/null
@@ -0,0 +1,297 @@
+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