]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
node-proxy: code change for hdd blinkenlight pre-requisites
authorGuillaume Abrioux <gabrioux@ibm.com>
Fri, 1 Dec 2023 08:18:25 +0000 (08:18 +0000)
committerGuillaume Abrioux <gabrioux@ibm.com>
Thu, 25 Jan 2024 16:01:04 +0000 (16:01 +0000)
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 <gabrioux@ibm.com>
(cherry picked from commit febfe0bf7588705785047bec49bf1a970ce180eb)

src/cephadm/cephadmlib/node_proxy/baseredfishsystem.py
src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py
src/pybind/mgr/cephadm/agent.py
src/pybind/mgr/cephadm/tests/test_node_proxy.py

index c4675e5b8f03d529e8d28846b143157c8f569787..44bb3427b71355a4ca4ac6ae4dc81d974cdd2411 100644 (file)
@@ -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
 
index 8bf4bc6befdf22f1c1ded15acadf23aa327adbc4..b41ade2e68fd9f97c5f47e0ea7b1b906af9116e9 100644 (file)
@@ -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']
index 03266a6c7a75f462e75f521caf606005b426bdaa..b760fcfb93d16571aef51ae1c6e8dc95b9363f2e 100644 (file)
@@ -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}/<endpoint>
+            hostname = vpath.pop(0)  # /<endpoint>
             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
+        # /<endpoint>
         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']}
index fce7152bd86ba479aaf1d83aa06b270096cff22d..1ca8b762cd39b8a0ee0ff9897bc7b6fda2c6f52a 100644 (file)
@@ -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")