From da08e9c45b6ca9c8c16615dfbf30451a50ccf449 Mon Sep 17 00:00:00 2001 From: Guillaume Abrioux Date: Fri, 1 Dec 2023 08:18:25 +0000 Subject: [PATCH] node-proxy: code change for hdd blinkenlight pre-requisites This is mainly for anticipating the case where hdd blinkenlight via RedFish works (testing has to be done). This introduces the required changes so the endpoint `/led` can support blinkenlight for both chassis and disks. Signed-off-by: Guillaume Abrioux (cherry picked from commit febfe0bf7588705785047bec49bf1a970ce180eb) --- .../node_proxy/baseredfishsystem.py | 3 +- .../node_proxy/redfishdellsystem.py | 1 + src/pybind/mgr/cephadm/agent.py | 51 ++++++- .../mgr/cephadm/tests/test_node_proxy.py | 129 ++++++++++++------ 4 files changed, 139 insertions(+), 45 deletions(-) diff --git a/src/cephadm/cephadmlib/node_proxy/baseredfishsystem.py b/src/cephadm/cephadmlib/node_proxy/baseredfishsystem.py index c4675e5b8f0..44bb3427b71 100644 --- a/src/cephadm/cephadmlib/node_proxy/baseredfishsystem.py +++ b/src/cephadm/cephadmlib/node_proxy/baseredfishsystem.py @@ -116,7 +116,8 @@ class BaseRedfishSystem(BaseSystem): 'power': self.get_power(), 'fans': self.get_fans() }, - 'firmwares': self.get_firmwares() + 'firmwares': self.get_firmwares(), + 'chassis': {'redfish_endpoint': f'/redfish/v1{self.chassis_endpoint}'} # TODO(guits): not ideal } return result diff --git a/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py b/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py index 8bf4bc6befd..b41ade2e68f 100644 --- a/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py +++ b/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py @@ -104,6 +104,7 @@ class RedfishDellSystem(BaseRedfishSystem): drive_info = self._get_path(drive_path) drive_id = drive_info['Id'] result[drive_id] = dict() + result[drive_id]['redfish_endpoint'] = drive['@odata.id'] for field in fields: result[drive_id][to_snake_case(field)] = drive_info[field] result[drive_id]['entity'] = entity['Id'] diff --git a/src/pybind/mgr/cephadm/agent.py b/src/pybind/mgr/cephadm/agent.py index 03266a6c7a7..b760fcfb93d 100644 --- a/src/pybind/mgr/cephadm/agent.py +++ b/src/pybind/mgr/cephadm/agent.py @@ -110,9 +110,19 @@ class NodeProxy: self.redfish_session_location: str = '' def _cp_dispatch(self, vpath: List[str]) -> "NodeProxy": - if len(vpath) == 2: - hostname = vpath.pop(0) + if len(vpath) > 1: # /{hostname}/ + hostname = vpath.pop(0) # / cherrypy.request.params['hostname'] = hostname + # /{hostname}/led/{type}/{drive} eg: /{hostname}/led/chassis or /{hostname}/led/drive/{id} + if vpath[0] == 'led' and len(vpath) > 1: # /led/{type}/{id} + _type = vpath[1] + cherrypy.request.params['type'] = _type + vpath.pop(1) # /led/{id} or # /led + if _type == 'drive' and len(vpath) > 1: # /led/{id} + _id = vpath[1] + vpath.pop(1) # /led + cherrypy.request.params['id'] = _id + # / return self @cherrypy.expose @@ -331,6 +341,7 @@ class NodeProxy: url = f'https://{addr}:{port}{endpoint}' _headers = headers response_json = {} + response_headers = {} if not _headers.get('Content-Type'): # default to application/json if nothing provided _headers['Content-Type'] = 'application/json' @@ -379,12 +390,34 @@ class NodeProxy: """ method: str = cherrypy.request.method hostname: Optional[str] = kw.get('hostname') + led_type: Optional[str] = kw.get('type') + id_drive: Optional[str] = kw.get('id') if not hostname: msg: str = "listing enclosure LED status for all nodes is not implemented." self.mgr.log.debug(msg) raise cherrypy.HTTPError(501, msg) + if not led_type: + msg = "the led type must be provided (either 'chassis' or 'drive')." + self.mgr.log.debug(msg) + raise cherrypy.HTTPError(400, msg) + + if led_type == 'drive' and not id_drive: + msg = "the id of the drive must be provided when type is 'drive'." + self.mgr.log.debug(msg) + raise cherrypy.HTTPError(400, msg) + + if hostname not in self.mgr.node_proxy.data.keys(): + # TODO(guits): update unit test for this + msg = f"'{hostname}' not found." + self.mgr.log.debug(msg) + raise cherrypy.HTTPError(400, msg) + + # if led_type not in ['chassis', 'drive']: + # # TODO(guits): update unit test for this + # raise cherrypy.HTTPError(404, 'LED type must be either "chassis" or "drive"') + addr = self.mgr.node_proxy.oob[hostname]['addr'] port = self.mgr.node_proxy.oob[hostname]['port'] username = self.mgr.node_proxy.oob[hostname]['username'] @@ -395,6 +428,16 @@ class NodeProxy: # allowing a specific keyring only ? (client.admin or client.agent.. ?) data: str = json.dumps(cherrypy.request.json) + if led_type == 'drive': + if id_drive not in self.mgr.node_proxy.data[hostname]['status']['storage'].keys(): + # TODO(guits): update unit test for this + msg = f"'{id_drive}' not found." + self.mgr.log.debug(msg) + raise cherrypy.HTTPError(400, msg) + endpoint = self.mgr.node_proxy.data[hostname]['status']['storage'][id_drive].get('redfish_endpoint') + else: + endpoint = self.mgr.node_proxy.data[hostname]['chassis']['redfish_endpoint'] + with self.redfish_session(addr, username, password, port=port): try: status, result, _ = self.query(data=bytes(data, 'ascii'), @@ -402,9 +445,9 @@ class NodeProxy: method=method, headers={"X-Auth-Token": self.redfish_token}, port=port, - endpoint='/redfish/v1/Chassis/System.Embedded.1', + endpoint=endpoint, ssl_ctx=self.ssl_ctx) - except (URLError, HTTPError) as e: + except (URLError, HTTPError, RuntimeError) as e: raise cherrypy.HTTPError(502, f"{e}") if method == 'GET': result = {"LocationIndicatorActive": result['LocationIndicatorActive']} diff --git a/src/pybind/mgr/cephadm/tests/test_node_proxy.py b/src/pybind/mgr/cephadm/tests/test_node_proxy.py index fce7152bd86..1ca8b762cd3 100644 --- a/src/pybind/mgr/cephadm/tests/test_node_proxy.py +++ b/src/pybind/mgr/cephadm/tests/test_node_proxy.py @@ -4,11 +4,9 @@ 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 unittest.mock import MagicMock, call, patch 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 @@ -23,6 +21,7 @@ class FakeMgr: self.remove_health_warning = MagicMock() self.inventory = Inventory(self) self.agent_cache = AgentCache(self) + self.agent_cache.agent_ports = {"host01": 1234} self.node_proxy = NodeProxyCache(self) self.node_proxy.save = MagicMock() self.http_server = MagicMock() @@ -133,48 +132,98 @@ class TestNodeProxy(helper.CPWebCase): ('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")) + @patch('cephadm.agent.AgentMessageThread.join', return_value=MagicMock) + @patch('cephadm.agent.AgentMessageThread.start', return_value=MagicMock) + def test_set_led_no_type(self, m_agent_msg_thread_start, m_agent_msg_thread_join): + data = '{"IndicatorLED": "Blinking"}' self.getPage("/host01/led", method="PATCH", body=data, headers=[('Content-Type', 'application/json'), ('Content-Length', str(len(data)))]) + self.assertStatus('400 Bad Request') - 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")) + @patch('cephadm.agent.AgentMessageThread.join', return_value=MagicMock) + @patch('cephadm.agent.AgentMessageThread.start', return_value=MagicMock) + def test_set_chassis_led(self, m_agent_msg_thread_start, m_agent_msg_thread_join): + data = '{"IndicatorLED": "Blinking"}' + with patch('cephadm.agent.AgentMessageThread.get_agent_response') as a: + a.return_value = '{"http_code": 200}' + self.getPage("/host01/led/chassis", method="PATCH", body=data, headers=[('Content-Type', 'application/json'), + ('Content-Length', str(len(data)))]) + self.assertStatus('200 OK') + + def test_get_led_missing_type(self): 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('400 Bad Request') + + def test_get_led_no_hostname(self): + self.getPage("/led", method="GET") + self.assertStatus('501 Not Implemented') + + def test_get_led_type_chassis_no_hostname(self): + self.getPage("/led/chassis", method="GET") + self.assertStatus('404 Not Found') + + def test_get_led_type_drive_no_hostname(self): + self.getPage("/led/chassis", method="GET") + self.assertStatus('404 Not Found') + + def test_get_led_type_drive_missing_id(self): + self.getPage("/host01/led/drive", method="GET") + self.assertStatus('400 Bad Request') + + def test_get_led_type_chassis_answer_invalid_json(self): + self.getPage("/host01/led/chassis", method="GET") + self.assertStatus('503 Service Unavailable') + + @patch('cephadm.agent.AgentMessageThread.join', return_value=MagicMock) + @patch('cephadm.agent.AgentMessageThread.start', return_value=MagicMock) + def test_get_led_type_chassis_answer_no_http_code(self, m_agent_msg_thread_start, m_agent_msg_thread_join): + with patch('cephadm.agent.AgentMessageThread.get_agent_response') as a: + a.return_value = '{"foo": "bar"}' + self.getPage("/host01/led/chassis", method="GET") + self.assertStatus('503 Service Unavailable') + + def test_get_led_status_not_200(self): + self.getPage("/host01/led/chassis", method="GET") + self.assertStatus('503 Service Unavailable') + + def test_get_led_key_error(self): + self.getPage("/host02/led/chassis", method="GET") self.assertStatus('502 Bad Gateway') - assert TestNodeProxy.app.query_endpoint.mock_calls == calls + + @patch('cephadm.agent.AgentMessageThread.join', return_value=MagicMock) + @patch('cephadm.agent.AgentMessageThread.start', return_value=MagicMock) + def test_get_chassis_led_ok(self, m_agent_msg_thread_start, m_agent_msg_thread_join): + with patch('cephadm.agent.AgentMessageThread.get_agent_response') as a: + a.return_value = '{"http_code": 200}' + self.getPage("/host01/led/chassis", method="GET") + self.assertStatus('200 OK') + + @patch('cephadm.agent.AgentMessageThread.join', return_value=MagicMock) + @patch('cephadm.agent.AgentMessageThread.start', return_value=MagicMock) + def test_get_drive_led_without_id(self, m_agent_msg_thread_start, m_agent_msg_thread_join): + self.getPage("/host01/led/drive", method="GET") + self.assertStatus('400 Bad Request') + + @patch('cephadm.agent.AgentMessageThread.join', return_value=MagicMock) + @patch('cephadm.agent.AgentMessageThread.start', return_value=MagicMock) + def test_get_drive_led_with_id(self, m_agent_msg_thread_start, m_agent_msg_thread_join): + with patch('cephadm.agent.AgentMessageThread.get_agent_response') as a: + a.return_value = '{"http_code": 200}' + self.getPage("/host01/led/drive/123", method="GET") + self.assertStatus('200 OK') + + # 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") -- 2.39.5