]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
node-proxy: drop local API
authorGuillaume Abrioux <gabrioux@ibm.com>
Tue, 28 Nov 2023 13:17:47 +0000 (13:17 +0000)
committerGuillaume Abrioux <gabrioux@ibm.com>
Thu, 25 Jan 2024 15:16:09 +0000 (15:16 +0000)
This was intented to address the case where the Ceph
manager can't talk directly to the oob management tool because
of network restrictions (subnets not inter-connecter, etc.).

If for any reason the host is stuck or unreachable, that local API won't
be helpful anyway, as a result any actions the Ceph mgr would be asked
to perform on the node would fail.

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

src/cephadm/cephadm.py
src/cephadm/cephadmlib/node_proxy/main.py
src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py
src/pybind/mgr/cephadm/agent.py

index d69f15a2bcb2063e04cdcfe7153fbc4e1989a646..e42647ebd8cf2d3420a2e18d3d6182f4bbcbc079 100755 (executable)
@@ -4966,10 +4966,7 @@ WantedBy=ceph-{fsid}.target
             'password': result_json['result']['password'],
             'cephx': node_proxy_meta['cephx'],
             'mgr_target_ip': self.target_ip,
-            'mgr_target_port': self.target_port,
-            # re-use listener ssl certificate instead of generating new ones...
-            'ssl_crt_path': self.listener_cert_path,
-            'ssl_key_path': self.listener_key_path
+            'mgr_target_port': self.target_port
         }
         if result_json['result'].get('port'):
             kwargs['port'] = result_json['result']['port']
index 66da0038ecc98d92638c142dc5a4982f7be1be97..0575340d0ecd20dddbc49e12dab9da454c74a8c0 100644 (file)
@@ -1,11 +1,8 @@
-import cherrypy
-from cherrypy._cpserver import Server
-from threading import Thread, Event
+from threading import Thread
 from .redfishdellsystem import RedfishDellSystem
 from .reporter import Reporter
 from .util import Config, Logger
-from typing import Dict, Any, Optional, List
-from .basesystem import BaseSystem
+from typing import Dict, Any, Optional
 import traceback
 
 DEFAULT_CONFIG = {
@@ -34,147 +31,12 @@ class NodeProxyFetchOobError(Exception):
     pass
 
 
-@cherrypy.tools.auth_basic(on=True)
-@cherrypy.tools.allow(methods=['PUT'])
-@cherrypy.tools.json_out()
-class Admin():
-    def __init__(self, api: 'API') -> None:
-        self.api = api
-
-    @cherrypy.expose
-    def start(self) -> Dict[str, str]:
-        self.api.backend.start_client()
-        # self.backend.start_update_loop()
-        self.api.reporter.run()
-        return {"ok": "node-proxy daemon started"}
-
-    @cherrypy.expose
-    def reload(self) -> Dict[str, str]:
-        self.api.config.reload()
-        return {"ok": "node-proxy config reloaded"}
-
-    def _stop(self) -> None:
-        self.api.backend.stop_update_loop()
-        self.api.backend.client.logout()
-        self.api.reporter.stop()
-
-    @cherrypy.expose
-    def stop(self) -> Dict[str, str]:
-        self._stop()
-        return {"ok": "node-proxy daemon stopped"}
-
-    @cherrypy.expose
-    def shutdown(self) -> Dict[str, str]:
-        self._stop()
-        cherrypy.engine.exit()
-        return {"ok": "Server shutdown."}
-
-    @cherrypy.expose
-    def flush(self) -> Dict[str, str]:
-        self.api.backend.flush()
-        return {"ok": "node-proxy data flushed"}
-
-
-class API(Server):
-    def __init__(self,
-                 backend: BaseSystem,
-                 reporter: Reporter,
-                 config: Config,
-                 addr: str = '0.0.0.0',
-                 port: int = 0) -> None:
-        super().__init__()
-        self.log = Logger(__name__)
-        self.backend = backend
-        self.reporter = reporter
-        self.config = config
-        self.socket_port = self.config.__dict__['server']['port'] if not port else port
-        self.socket_host = addr
-        self.subscribe()
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def memory(self) -> Dict[str, Any]:
-        return {'memory': self.backend.get_memory()}
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def network(self) -> Dict[str, Any]:
-        return {'network': self.backend.get_network()}
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def processors(self) -> Dict[str, Any]:
-        return {'processors': self.backend.get_processors()}
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def storage(self) -> Dict[str, Any]:
-        return {'storage': self.backend.get_storage()}
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def power(self) -> Dict[str, Any]:
-        return {'power': self.backend.get_power()}
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def fans(self) -> Dict[str, Any]:
-        return {'fans': self.backend.get_fans()}
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def firmwares(self) -> Dict[str, Any]:
-        return {'firmwares': self.backend.get_firmwares()}
-
-    def _cp_dispatch(self, vpath: List[str]) -> "API":
-        if vpath[0] == 'led':
-            if cherrypy.request.method == 'GET':
-                return self.get_led
-            if cherrypy.request.method == 'PATCH':
-                return self.set_led
-        return self
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['GET'])
-    @cherrypy.tools.json_out()
-    def get_led(self, **kw: Dict[str, Any]) -> Dict[str, Any]:
-        return self.backend.get_led()
-
-    @cherrypy.expose
-    @cherrypy.tools.allow(methods=['PATCH'])
-    @cherrypy.tools.json_in()
-    @cherrypy.tools.json_out()
-    @cherrypy.tools.auth_basic(on=True)
-    def set_led(self, **kw: Dict[str, Any]) -> Dict[str, Any]:
-        data = cherrypy.request.json
-        rc = self.backend.set_led(data)
-
-        if rc != 200:
-            cherrypy.response.status = rc
-            result = {"state": f"error: please, verify the data you sent."}
-        else:
-            result = {"state": data["state"].lower()}
-        return result
-
-    def stop(self) -> None:
-        self.unsubscribe()
-        super().stop()
-
-
 class NodeProxy(Thread):
     def __init__(self, **kw: Dict[str, Any]) -> None:
         super().__init__()
         for k, v in kw.items():
             setattr(self, k, v)
         self.exc: Optional[Exception] = None
-        self.cp_shutdown_event = Event()
         self.log = Logger(__name__)
 
     def run(self) -> None:
@@ -197,16 +59,6 @@ class NodeProxy(Thread):
             raise self.exc
         return True
 
-    def start_api(self) -> None:
-        cherrypy.server.unsubscribe()
-        cherrypy.engine.start()
-        self.reporter_agent.run()
-        self.cp_shutdown_event.wait()
-        self.cp_shutdown_event.clear()
-        cherrypy.engine.stop()
-        cherrypy.server.httpserver = None
-        self.log.logger.info("node-proxy shutdown.")
-
     def main(self) -> None:
         # TODO: add a check and fail if host/username/password/data aren't passed
         self.config = Config('/etc/ceph/node-proxy.yml', default_config=DEFAULT_CONFIG)
@@ -231,32 +83,7 @@ class NodeProxy(Thread):
                                            reporter_hostname=self.__dict__['mgr_target_ip'],
                                            reporter_port=self.__dict__['mgr_target_port'],
                                            reporter_endpoint=self.__dict__.get('reporter_endpoint', '/node-proxy/data'))
+            self.reporter_agent.run()
         except RuntimeError:
             self.log.logger.error("Can't initialize the reporter.")
             raise
-        self.api = API(self.system,
-                       self.reporter_agent,
-                       self.config)
-        self.admin = Admin(self.api)
-        self.configure()
-        self.start_api()
-
-    def configure(self) -> None:
-        cherrypy.config.update({
-            'environment': 'production',
-            'engine.autoreload.on': False,
-        })
-        config = {'/': {
-            'request.methods_with_bodies': ('POST', 'PUT', 'PATCH'),
-            'tools.trailing_slash.on': False,
-            'tools.auth_basic.realm': 'localhost',
-            'tools.auth_basic.checkpassword': self.check_auth
-        }}
-        cherrypy.tree.mount(self.api, '/', config=config)
-        cherrypy.tree.mount(self.admin, '/admin', config=config)
-        self.api.ssl_certificate = self.__dict__['ssl_crt_path']
-        self.api.ssl_private_key = self.__dict__['ssl_key_path']
-
-    def shutdown(self) -> None:
-        self.log.logger.info("Shutting node-proxy down...")
-        self.cp_shutdown_event.set()
index 516272c7223e8741f9c237323079661e0f95a36d..8bf4bc6befdf22f1c1ded15acadf23aa327adbc4 100644 (file)
@@ -1,4 +1,3 @@
-import json
 from .baseredfishsystem import BaseRedfishSystem
 from .util import Logger, normalize_dict, to_snake_case
 from typing import Dict, Any, List
@@ -69,42 +68,6 @@ class RedfishDellSystem(BaseRedfishSystem):
     def get_fans(self) -> Dict[str, Dict[str, Dict]]:
         return self._sys['fans']
 
-    def get_led(self) -> Dict[str, Any]:
-        endpoint = f"/redfish/v1/{self.chassis_endpoint}"
-        result = self.client.query(method='GET',
-                                   endpoint=endpoint,
-                                   timeout=10)
-        response_json = json.loads(result[1])
-        mapper = {
-            'true': 'on',
-            'false': 'off'
-        }
-        if result[2] == 200:
-            return {"state": mapper[str(response_json['LocationIndicatorActive']).lower()]}
-        else:
-            return {"error": "Couldn't retrieve enclosure LED status."}
-
-    def set_led(self, data: Dict[str, str]) -> int:
-        # '{"IndicatorLED": "Lit"}'      -> LocationIndicatorActive = false
-        # '{"IndicatorLED": "Blinking"}' -> LocationIndicatorActive = true
-        mapper = {
-            "on": 'Blinking',
-            "off": 'Lit'
-        }
-        try:
-            _data = {
-                "IndicatorLED": mapper[data["state"].lower()]
-            }
-            _, response, status = self.client.query(
-                data=json.dumps(_data),
-                method='PATCH',
-                endpoint=f"/redfish/v1{self.chassis_endpoint}"
-            )
-        except KeyError:
-            status = 400
-        result = status
-        return result
-
     def _update_network(self) -> None:
         fields = ['Description', 'Name', 'SpeedMbps', 'Status']
         self.log.logger.debug('Updating network')
index d5563eee6c740a23d3e74fa325cfeb91e3c46962..d5f7d3161cf9f9c85dbb38860728627dc68c350e 100644 (file)
@@ -13,7 +13,6 @@ import ssl
 import tempfile
 import threading
 import time
-import base64
 
 from orchestrator import DaemonDescriptionStatus
 from orchestrator._interface import daemon_type_to_service
@@ -25,8 +24,9 @@ from cephadm.ssl_cert_utils import SSLCerts
 from mgr_util import test_port_allocation, PortAlreadyInUse
 from urllib.request import urlopen, Request
 from urllib.error import HTTPError, URLError
+from contextlib import contextmanager
 
-from typing import Any, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
+from typing import Any, Dict, List, Set, Tuple, TYPE_CHECKING, Optional, Generator
 
 if TYPE_CHECKING:
     from cephadm.module import CephadmOrchestrator
@@ -99,10 +99,15 @@ class NodeProxy:
     def __init__(self, mgr: "CephadmOrchestrator"):
         self.mgr = mgr
         self.ssl_root_crt = self.mgr.http_server.agent.ssl_certs.get_root_cert()
-        self.ssl_ctx = ssl.create_default_context()
-        self.ssl_ctx.check_hostname = True
-        self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
-        self.ssl_ctx.load_verify_locations(cadata=self.ssl_root_crt)
+        self.ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        self.ssl_ctx.check_hostname = False
+        self.ssl_ctx.verify_mode = ssl.CERT_NONE
+        # self.ssl_ctx = ssl.create_default_context()
+        # self.ssl_ctx.check_hostname = True
+        # self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
+        # self.ssl_ctx.load_verify_locations(cadata=self.ssl_root_crt)
+        self.redfish_token: str = ''
+        self.redfish_session_location: str = ''
 
     def _cp_dispatch(self, vpath: List[str]) -> "NodeProxy":
         if len(vpath) == 2:
@@ -252,15 +257,77 @@ class NodeProxy:
         self.mgr.node_proxy.save(host, data['patch'])
         self.raise_alert(data)
 
-    def query_endpoint(self,
-                       addr: str = '',
-                       port: str = '',
-                       method: Optional[str] = None,
-                       headers: Optional[Dict[str, str]] = {},
-                       data: Optional[bytes] = None,
-                       endpoint: str = '',
-                       ssl_ctx: Optional[Any] = None,
-                       timeout: Optional[int] = 10) -> Tuple[int, Dict[str, Any]]:
+    def login(self,
+              addr: str,
+              username: str,
+              password: str,
+              port: str = '443') -> None:
+        self.mgr.log.info("Logging in to "
+                          f"{addr}:{port} as '{username}'")
+        oob_credentials = json.dumps({"UserName": username,
+                                      "Password": password})
+        headers = {"Content-Type": "application/json"}
+
+        try:
+            _status_code, _data, _headers = self.query(addr=addr,
+                                                       port=port,
+                                                       data=oob_credentials,
+                                                       headers=headers,
+                                                       endpoint="/redfish/v1/SessionService/Sessions/",
+                                                       method="POST")
+            if _status_code != 201:
+                self.mgr.log.error(f"Can't log in to {addr}:{port} as '{username}': {_status_code}")
+                raise RuntimeError
+            self.redfish_token = _headers['X-Auth-Token']
+            self.redfish_session_location = _headers['Location']
+        except URLError as e:
+            msg = f"Can't log in to {addr}:{port} as '{username}': {e}"
+            self.mgr.log.error(msg)
+            raise RuntimeError
+
+    def logout(self,
+               addr: str,
+               port: str = '443') -> None:
+        try:
+            _status_code, _data, _ = self.query(addr=addr,
+                                                port=port,
+                                                method='DELETE',
+                                                headers={"X-Auth-Token": self.redfish_token},
+                                                endpoint=self.redfish_session_location)
+        except URLError:
+            msg = f"Can't log out from {addr}:{port}"
+            self.mgr.log.error(msg)
+            raise cherrypy.HTTPError(502, msg)
+
+    @contextmanager
+    def redfish_session(self,
+                        addr: str,
+                        username: str,
+                        password: str,
+                        port: str = '443') -> Generator:
+        try:
+            self.login(addr=addr,
+                       port=port,
+                       username=username,
+                       password=password)
+            yield
+        except Exception:
+            raise
+        else:
+            self.logout(addr=addr,
+                        port=port)
+
+    def query(self,
+              addr: str = '',
+              port: str = '',
+              method: Optional[str] = None,
+              headers: Dict[str, str] = {},
+              data: Optional[bytes] = None,
+              endpoint: str = '',
+              ssl_ctx: Optional[Any] = None,
+              timeout: Optional[int] = 10) -> Tuple[int, Dict[str, Any], Dict[str, Any]]:
+        if not ssl_ctx:
+            ssl_ctx = self.ssl_ctx
         url = f'https://{addr}:{port}{endpoint}'
         _headers = headers
         response_json = {}
@@ -272,18 +339,19 @@ class NodeProxy:
             req = Request(url, _data, _headers, method=method)
             with urlopen(req, context=ssl_ctx, timeout=timeout) as response:
                 response_str = response.read()
+                response_headers = response.headers
                 response_json = json.loads(response_str)
                 response_status = response.status
         except HTTPError as e:
-            self.mgr.log.debug(f"{e.code} {e.reason}")
+            self.mgr.log.error(f"{e.code} {e.reason}")
             response_status = e.code
         except URLError as e:
-            self.mgr.log.debug(f"{e.reason}")
+            self.mgr.log.error(f"{e.reason}")
             raise
         except Exception as e:
             self.mgr.log.error(f"{e}")
             raise
-        return (response_status, response_json)
+        return (response_status, response_json, response_headers)
 
     @cherrypy.expose
     @cherrypy.tools.allow(methods=['GET', 'PATCH'])
@@ -311,37 +379,37 @@ class NodeProxy:
         """
         method: str = cherrypy.request.method
         hostname: Optional[str] = kw.get('hostname')
-        headers: Dict[str, str] = {}
 
         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)
 
-        addr = self.mgr.inventory.get_addr(hostname)
+        addr = self.mgr.node_proxy.oob[hostname]['addr']
+        port = self.mgr.node_proxy.oob[hostname]['port']
+        username = self.mgr.node_proxy.oob[hostname]['username']
+        password = self.mgr.node_proxy.oob[hostname]['password']
 
         if method == 'PATCH':
             # TODO(guits): need to check the request is authorized
             # allowing a specific keyring only ? (client.admin or client.agent.. ?)
             data: str = json.dumps(cherrypy.request.json)
-            username = self.mgr.node_proxy.oob[hostname]['username']
-            password = self.mgr.node_proxy.oob[hostname]['password']
-            auth = f"{username}:{password}".encode("utf-8")
-            auth = base64.b64encode(auth).decode("utf-8")
-            headers = {"Authorization": f"Basic {auth}"}
 
-        try:
-            status, result = self.query_endpoint(data=data,
-                                                headers=headers,
-                                                addr=addr,
-                                                method=method,
-                                                port=8080,
-                                                endpoint='/led',
-                                                ssl_ctx=self.ssl_ctx)
-        except URLError as e:
-            raise cherrypy.HTTPError(502, f"{e}")
-        cherrypy.response.status = status
-        return result
+        with self.redfish_session(addr, username, password, port=port):
+            try:
+                status, result, _ = self.query(data=bytes(data, 'ascii'),
+                                               addr=addr,
+                                               method=method,
+                                               headers={"X-Auth-Token": self.redfish_token},
+                                               port=port,
+                                               endpoint='/redfish/v1/Chassis/System.Embedded.1',
+                                               ssl_ctx=self.ssl_ctx)
+            except (URLError, HTTPError) as e:
+                raise cherrypy.HTTPError(502, f"{e}")
+            if method == 'GET':
+                result = {"LocationIndicatorActive": result['LocationIndicatorActive']}
+            cherrypy.response.status = status
+            return result
 
     @cherrypy.expose
     @cherrypy.tools.allow(methods=['GET'])