'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']
-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 = {
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:
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)
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()
-import json
from .baseredfishsystem import BaseRedfishSystem
from .util import Logger, normalize_dict, to_snake_case
from typing import Dict, Any, List
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')
import tempfile
import threading
import time
-import base64
from orchestrator import DaemonDescriptionStatus
from orchestrator._interface import daemon_type_to_service
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
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:
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 = {}
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'])
"""
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'])