From d20583e82d38875de0ab7f8f6f46c7e72bf47b21 Mon Sep 17 00:00:00 2001 From: Guillaume Abrioux Date: Tue, 28 Nov 2023 13:17:47 +0000 Subject: [PATCH] node-proxy: drop local API 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 (cherry picked from commit 3607a305cf629f54a8c3c3f52e56d41210c21d28) --- src/cephadm/cephadm.py | 5 +- src/cephadm/cephadmlib/node_proxy/main.py | 179 +----------------- .../node_proxy/redfishdellsystem.py | 37 ---- src/pybind/mgr/cephadm/agent.py | 142 ++++++++++---- 4 files changed, 109 insertions(+), 254 deletions(-) diff --git a/src/cephadm/cephadm.py b/src/cephadm/cephadm.py index d69f15a2bcb20..e42647ebd8cf2 100755 --- a/src/cephadm/cephadm.py +++ b/src/cephadm/cephadm.py @@ -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'] diff --git a/src/cephadm/cephadmlib/node_proxy/main.py b/src/cephadm/cephadmlib/node_proxy/main.py index 66da0038ecc98..0575340d0ecd2 100644 --- a/src/cephadm/cephadmlib/node_proxy/main.py +++ b/src/cephadm/cephadmlib/node_proxy/main.py @@ -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() diff --git a/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py b/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py index 516272c7223e8..8bf4bc6befdf2 100644 --- a/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py +++ b/src/cephadm/cephadmlib/node_proxy/redfishdellsystem.py @@ -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') diff --git a/src/pybind/mgr/cephadm/agent.py b/src/pybind/mgr/cephadm/agent.py index d5563eee6c740..d5f7d3161cf9f 100644 --- a/src/pybind/mgr/cephadm/agent.py +++ b/src/pybind/mgr/cephadm/agent.py @@ -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']) -- 2.39.5