From 54b06d7823eb122fa96d13bf9acdbc8b865f6602 Mon Sep 17 00:00:00 2001 From: hamdykhader Date: Thu, 16 Apr 2026 00:05:55 +0300 Subject: [PATCH 1/4] Security Enhancement -Adds token auth to snode api -Refactor the code to use snode_client() - new parameter required when adding node 'sn add --access-token' - This access token can be set when deploying node (sn deploy --access-token) --- requirements.txt | 3 +- simplyblock_cli/cli-reference.yaml | 8 ++++ simplyblock_cli/cli.py | 2 + simplyblock_cli/clibase.py | 4 +- .../controllers/device_controller.py | 4 +- .../controllers/health_controller.py | 19 ++------ .../controllers/lvol_controller.py | 4 +- simplyblock_core/env_var | 2 +- simplyblock_core/models/storage_node.py | 8 ++++ .../services/health_check_service.py | 2 +- .../services/storage_node_monitor.py | 4 +- .../services/tasks_runner_restart.py | 2 +- simplyblock_core/snode_client.py | 6 +-- simplyblock_core/storage_node_ops.py | 46 ++++++++++--------- simplyblock_web/api/v1/storage_node.py | 5 ++ simplyblock_web/api/v2/storage_node.py | 2 + simplyblock_web/node_webapp.py | 25 ++++++++-- 17 files changed, 93 insertions(+), 53 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2bd6493f1..6e78f72e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ jsonschema fastapi uvicorn prometheus_api_client -paramiko \ No newline at end of file +paramiko +flask_httpauth \ No newline at end of file diff --git a/simplyblock_cli/cli-reference.yaml b/simplyblock_cli/cli-reference.yaml index 55bb158d8..4cfcbd32b 100644 --- a/simplyblock_cli/cli-reference.yaml +++ b/simplyblock_cli/cli-reference.yaml @@ -26,6 +26,10 @@ commands: type: bool default: false action: store_true + - name: "--access-token" + help: "Storage node API access token." + dest: access_token + type: str - name: configure help: "Prepare a configuration file to be used when adding the storage node." description: > @@ -265,6 +269,10 @@ commands: dest: spdk_proxy_image type: str private: true + - name: "--access-token" + help: "Storage node API access token." + dest: access_token + type: str - name: delete help: "Deletes a storage node object from the state database." usage: > diff --git a/simplyblock_cli/cli.py b/simplyblock_cli/cli.py index a0ce63247..4e3297bc8 100755 --- a/simplyblock_cli/cli.py +++ b/simplyblock_cli/cli.py @@ -90,6 +90,7 @@ def init_storage_node__deploy(self, subparser): subcommand = self.add_sub_command(subparser, 'deploy', 'Prepares a host to be used as a storage node.') argument = subcommand.add_argument('--ifname', help='Management interface name, e.g. eth0.', type=str, dest='ifname') argument = subcommand.add_argument('--isolate-cores', help='Isolates cores in kernel args for the provided CPU mask. Default: `false`.', default=False, dest='isolate_cores', action='store_true') + argument = subcommand.add_argument('--access-token', help='Storage node API access token.', type=str, dest='access_token') def init_storage_node__configure(self, subparser): subcommand = self.add_sub_command(subparser, 'configure', 'Prepare a configuration file to be used when adding the storage node.') @@ -151,6 +152,7 @@ def init_storage_node__add_node(self, subparser): argument = subcommand.add_argument('--max-snap', help='The max snapshot per storage node. Default: `5000`.', type=int, default=5000, dest='max_snap') if self.developer_mode: argument = subcommand.add_argument('--spdk-proxy-image', help='The SPDK proxy image URI.', type=str, dest='spdk_proxy_image') + argument = subcommand.add_argument('--access-token', help='Storage node API access token.', type=str, dest='access_token') def init_storage_node__delete(self, subparser): subcommand = self.add_sub_command(subparser, 'delete', 'Deletes a storage node object from the state database.') diff --git a/simplyblock_cli/clibase.py b/simplyblock_cli/clibase.py index 44e763574..c50beaf5f 100755 --- a/simplyblock_cli/clibase.py +++ b/simplyblock_cli/clibase.py @@ -79,8 +79,7 @@ def add_sub_command(self, parent_parser, command, help, usage=None): return parent_parser.add_parser(command, description=help, help=help, usage=usage) def storage_node__deploy(self, sub_command, args): - isolate_cores = args.isolate_cores - return storage_ops.deploy(args.ifname, isolate_cores) + return storage_ops.deploy(args.ifname, args.isolate_cores, args.access_token) def storage_node__configure_upgrade(self, sub_command, args): storage_ops.upgrade_automated_deployment_config() @@ -195,6 +194,7 @@ def storage_node__add_node(self, sub_command, args): ha_jm_count=ha_jm_count, format_4k=format_4k, spdk_proxy_image=getattr(args, 'spdk_proxy_image', None), + access_token=getattr(args, 'access_token', ""), ) except Exception as e: print(e) diff --git a/simplyblock_core/controllers/device_controller.py b/simplyblock_core/controllers/device_controller.py index 6b83e08e0..1aa734d64 100644 --- a/simplyblock_core/controllers/device_controller.py +++ b/simplyblock_core/controllers/device_controller.py @@ -260,7 +260,7 @@ def restart_device(device_id, force=False): if not snode.rpc_client().bdev_nvme_controller_list(device_obj.nvme_controller): try: - ret = SNodeClient(snode.api_endpoint, timeout=30, retry=1).bind_device_to_spdk(device_obj.pcie_address) + ret = snode.snode_client(timeout=30, retry=1).bind_device_to_spdk(device_obj.pcie_address) logger.debug(ret) snode.rpc_client().bdev_nvme_controller_attach(device_obj.nvme_controller, device_obj.pcie_address) snode.rpc_client().bdev_examine(f"{device_obj.nvme_controller}n1") @@ -970,7 +970,7 @@ def new_device_from_failed(device_id): if not device_node.rpc_client().bdev_nvme_controller_list(device.nvme_controller): try: - ret = SNodeClient(device_node.api_endpoint, timeout=30, retry=1).bind_device_to_spdk(device.pcie_address) + ret = device_node.snode_client(timeout=30, retry=1).bind_device_to_spdk(device.pcie_address) logger.debug(ret) device_node.rpc_client().bdev_nvme_controller_attach(device.nvme_controller, device.pcie_address) except Exception as e: diff --git a/simplyblock_core/controllers/health_controller.py b/simplyblock_core/controllers/health_controller.py index c4b474842..c636de9bf 100644 --- a/simplyblock_core/controllers/health_controller.py +++ b/simplyblock_core/controllers/health_controller.py @@ -117,10 +117,9 @@ def _check_node_rpc(rpc_ip, rpc_port, rpc_username, rpc_password, timeout=5, ret return False, False -def _check_node_api(ip): +def check_node_api(node): try: - snode_api = SNodeClient(f"{ip}:5000", timeout=90, retry=2) - logger.debug(f"Node API={ip}:5000") + snode_api = node.snode_client(timeout=90, retry=2) ret, _ = snode_api.is_live() logger.debug(f"snode is alive: {ret}") if ret: @@ -130,14 +129,6 @@ def _check_node_api(ip): return False -def _check_spdk_process_up(ip, rpc_port, cluster_id): - snode_api = SNodeClient(f"{ip}:5000", timeout=90, retry=2) - logger.debug(f"Node API={ip}:5000") - is_up, _ = snode_api.spdk_process_is_up(rpc_port, cluster_id) - logger.debug(f"SPDK is {is_up}") - return is_up - - def check_port_on_node(snode, port_id): fw_api = FirewallClient(snode, timeout=5, retry=2) command_output, _ = fw_api.get_firewall(snode.rpc_port) @@ -191,9 +182,9 @@ def _check_node_ping(ip): def _check_ping_from_node(ip, ifname, node): - snodeapi = SNodeClient(node.api_endpoint, timeout=3, retry=3) + snode_api = node.snode_client( timeout=3, retry=3) try: - ret, _ = snodeapi.ping_ip(ip, ifname) + ret, _ = snode_api.ping_ip(ip, ifname) return bool(ret) except Exception as e: logger.error(e) @@ -581,7 +572,7 @@ def check_node(node_id, with_devices=True): logger.info(f"Check: ping mgmt ip {snode.mgmt_ip} ... {ping_check}") # 2- check node API - node_api_check = _check_node_api(snode.mgmt_ip) + node_api_check = check_node_api(snode) logger.info(f"Check: node API {snode.mgmt_ip}:5000 ... {node_api_check}") # 3- check node RPC diff --git a/simplyblock_core/controllers/lvol_controller.py b/simplyblock_core/controllers/lvol_controller.py index 9ebc658de..ee158e48d 100644 --- a/simplyblock_core/controllers/lvol_controller.py +++ b/simplyblock_core/controllers/lvol_controller.py @@ -52,7 +52,7 @@ def _register_pool_dhchap_keys_on_node(pool, snode, rpc_client): Returns a dict with 'dhchap_key' and 'dhchap_ctrlr_key' keyring names, or an empty dict on failure. """ - snode_api = SNodeClient(snode.api_endpoint) + snode_api = snode.snode_client() safe_pool = pool.get_id().replace("-", "_") key_names = {} @@ -90,7 +90,7 @@ def _register_dhchap_keys_on_node(snode, host_nqn, host_entry, rpc_client): Returns a dict mapping key type ('dhchap_key', 'dhchap_ctrlr_key', 'psk') to the SPDK keyring name for use in subsystem_add_host. """ - snode_api = SNodeClient(snode.api_endpoint) + snode_api = snode.snode_client() # Sanitize host NQN for use as filename safe_host = host_nqn.replace(":", "_").replace(".", "_") key_names = {} diff --git a/simplyblock_core/env_var b/simplyblock_core/env_var index f6ec62a06..262f4b572 100644 --- a/simplyblock_core/env_var +++ b/simplyblock_core/env_var @@ -1,5 +1,5 @@ SIMPLY_BLOCK_COMMAND_NAME=sbcli-dev SIMPLY_BLOCK_VERSION=19.2.33 -SIMPLY_BLOCK_DOCKER_IMAGE=public.ecr.aws/simply-block/simplyblock:main +SIMPLY_BLOCK_DOCKER_IMAGE=public.ecr.aws/simply-block/simplyblock:main-snode-auth SIMPLY_BLOCK_SPDK_ULTRA_IMAGE=public.ecr.aws/simply-block/ultra:main-latest diff --git a/simplyblock_core/models/storage_node.py b/simplyblock_core/models/storage_node.py index dba8b5f46..b94bfe712 100644 --- a/simplyblock_core/models/storage_node.py +++ b/simplyblock_core/models/storage_node.py @@ -10,6 +10,7 @@ from simplyblock_core.models.job_schedule import JobSchedule from simplyblock_core.models.nvme_device import NVMeDevice, JMDevice, RemoteDevice, RemoteJMDevice from simplyblock_core.rpc_client import RPCClient, RPCException +from simplyblock_core.snode_client import SNodeClient logger = utils.get_logger(__name__) @@ -112,6 +113,8 @@ class StorageNode(BaseNodeObject): firewall_port: int = 5001 lvol_poller_mask: str = "" spdk_proxy_image: str = "" + access_token = "" + def get_lvol_subsys_port(self, lvs_name=None): """Get the client-facing NVMeoF port for a specific lvstore. @@ -482,6 +485,11 @@ def lvol_del_sync_lock_reset(self) -> bool: time.sleep(0.250) return True + def snode_client(self, **kwargs): + """Return storage node api client to this node + """ + return SNodeClient(self.api_endpoint, self.access_token, **kwargs) + class NodeLVolDelLock(BaseModel): pass \ No newline at end of file diff --git a/simplyblock_core/services/health_check_service.py b/simplyblock_core/services/health_check_service.py index b30391be2..6b83411d2 100644 --- a/simplyblock_core/services/health_check_service.py +++ b/simplyblock_core/services/health_check_service.py @@ -67,7 +67,7 @@ def check_node(snode): logger.info(f"Check: ping mgmt ip {snode.mgmt_ip} ... {ping_check}") # 2- check node API - node_api_check = health_controller._check_node_api(snode.mgmt_ip) + node_api_check = health_controller.check_node_api(snode) logger.info(f"Check: node API {snode.mgmt_ip}:5000 ... {node_api_check}") # 3- check node RPC diff --git a/simplyblock_core/services/storage_node_monitor.py b/simplyblock_core/services/storage_node_monitor.py index 57347f476..008cd5f37 100644 --- a/simplyblock_core/services/storage_node_monitor.py +++ b/simplyblock_core/services/storage_node_monitor.py @@ -464,7 +464,7 @@ def check_node(snode): # 2- check node API try: - snode_api = SNodeClient(f"{snode.mgmt_ip}:5000", timeout=10, retry=2) + snode_api = snode.snode_client(timeout=10, retry=2) ret, _ = snode_api.is_live() logger.info(f"Check: node API {snode.mgmt_ip}:5000 ... {ret}") if not ret: @@ -478,7 +478,7 @@ def check_node(snode): # 3- check spdk process through node API try: - snode_api = SNodeClient(f"{snode.mgmt_ip}:5000", timeout=40, retry=2) + snode_api = snode.snode_client(timeout=40, retry=2) is_up, _ = snode_api.spdk_process_is_up( snode.rpc_port, snode.cluster_id) logger.info(f"Check: spdk process {snode.mgmt_ip}:5000 ... {bool(is_up)}") if not is_up: diff --git a/simplyblock_core/services/tasks_runner_restart.py b/simplyblock_core/services/tasks_runner_restart.py index 14a97757c..e29792827 100644 --- a/simplyblock_core/services/tasks_runner_restart.py +++ b/simplyblock_core/services/tasks_runner_restart.py @@ -176,7 +176,7 @@ def task_runner_node(task): # is node reachable? ping_check = health_controller._check_node_ping(node.mgmt_ip) logger.info(f"Check: ping mgmt ip {node.mgmt_ip} ... {ping_check}") - node_api_check = health_controller._check_node_api(node.mgmt_ip) + node_api_check = health_controller.check_node_api(node) logger.info(f"Check: node API {node.mgmt_ip}:5000 ... {node_api_check}") node_data_nic_ping_check = False for data_nic in node.data_nics: diff --git a/simplyblock_core/snode_client.py b/simplyblock_core/snode_client.py index bf4d10e14..32692cb5a 100644 --- a/simplyblock_core/snode_client.py +++ b/simplyblock_core/snode_client.py @@ -16,19 +16,19 @@ def __init__(self, message): class SNodeClient: - def __init__(self, ip_address, timeout=300, retry=5): + def __init__(self, ip_address, access_token="access_token", timeout=300, retry=5): self.ip_address = ip_address self.url = 'http://%s/snode/' % self.ip_address self.timeout = timeout self.session = requests.session() self.session.verify = False + self.session.auth = ("token", access_token) self.session.headers['Content-Type'] = "application/json" retries = Retry(total=retry, backoff_factor=1, connect=retry, read=retry) self.session.mount("http://", HTTPAdapter(max_retries=retries)) def _request(self, method, path, payload=None): try: - logger.debug("Requesting path: %s, params: %s", path, payload) data = None params = None if payload: @@ -36,7 +36,7 @@ def _request(self, method, path, payload=None): params = payload else: data = json.dumps(payload) - + logger.debug("Requesting: %s, params: %s", self.url+path, params) response = self.session.request(method, self.url+path, data=data, timeout=self.timeout, params=params) except Exception as e: diff --git a/simplyblock_core/storage_node_ops.py b/simplyblock_core/storage_node_ops.py index f155e00ab..56dfceb82 100755 --- a/simplyblock_core/storage_node_ops.py +++ b/simplyblock_core/storage_node_ops.py @@ -554,7 +554,7 @@ def _create_device_partitions(rpc_client, nvme, snode, num_partitions_per_dev, j if not nbd_device: logger.error("Failed to start nbd dev") return False - snode_api = SNodeClient(snode.api_endpoint) + snode_api = snode.snode_client() partition_percent = 0 if partition_size: partition_percent = int(partition_size * 100 / nvme.size) @@ -1058,8 +1058,9 @@ def add_node(cluster_id, node_addr, iface_name, data_nics_list, small_bufsize=0, large_bufsize=0, num_partitions_per_dev=0, jm_percent=0, enable_test_device=False, namespace=None, enable_ha_jm=False, cr_name=None, cr_namespace=None, cr_plural=None, - id_device_by_nqn=False, partition_size="", ha_jm_count=None, format_4k=False, spdk_proxy_image=None): - snode_api = SNodeClient(node_addr) + id_device_by_nqn=False, partition_size="", ha_jm_count=None, format_4k=False, spdk_proxy_image=None, + access_token=None): + snode_api = SNodeClient(node_addr, access_token) node_info, _ = snode_api.info() if node_info.get("nodes_config") and node_info["nodes_config"].get("nodes"): nodes = node_info["nodes_config"]["nodes"] @@ -1210,7 +1211,7 @@ def add_node(cluster_id, node_addr, iface_name, data_nics_list, log_config_type = utils.get_storage_node_api_log_type(mgmt_ip, '/SNodeAPI') if log_config_type and log_config_type != LogConfig.types.GELF: logger.info("SNodeAPI container found but not configured with gelf logger") - start_storage_node_api_container(mgmt_ip, cluster_ip) + start_storage_node_api_container(mgmt_ip, cluster_ip, access_token) node_socket = node_config.get("socket") total_mem = minimum_hp_memory @@ -1364,6 +1365,7 @@ def add_node(cluster_id, node_addr, iface_name, data_nics_list, snode.active_tcp = active_tcp snode.active_rdma = active_rdma snode.spdk_proxy_image = spdk_proxy_image + snode.access_token = access_token if 'cpu_count' in node_info: snode.cpu = node_info['cpu_count'] @@ -1749,9 +1751,9 @@ def remove_storage_node(node_id, force_remove=False, force_migrate=False): pass try: - if health_controller._check_node_api(snode.mgmt_ip): + if health_controller.check_node_api(snode): logger.info("Stopping SPDK container") - snode_api = SNodeClient(snode.api_endpoint, timeout=20) + snode_api = snode.snode_client(timeout=20) snode_api.spdk_process_kill(snode.rpc_port, snode.cluster_id) snode_api.leave_swarm() pci_address = [] @@ -1825,7 +1827,7 @@ def restart_storage_node( if node_ip: if node_ip != snode.api_endpoint: logger.info(f"Restarting on new node with ip: {node_ip}") - snode_api = SNodeClient(node_ip, timeout=5 * 60, retry=3) + snode_api = snode.snode_client(timeout=5 * 60, retry=3) node_info, _ = snode_api.info() if not node_info: logger.error("Failed to get node info!") @@ -1873,7 +1875,7 @@ def restart_storage_node( active_rdma = False fabric_tcp = cluster.fabric_tcp fabric_rdma = cluster.fabric_rdma - snode_api = SNodeClient(snode.api_endpoint, timeout=5 * 60, retry=3) + snode_api = snode.snode_client(timeout=5 * 60, retry=3) for nic in snode.data_nics: if fabric_rdma and snode_api.ifc_is_roce(nic["if_name"]): nic.trtype = "RDMA" @@ -2314,7 +2316,7 @@ def restart_storage_node( except Exception as e: logger.error(e) storage_events.snode_restart_failed(snode) - snode_api = SNodeClient(snode.api_endpoint, timeout=5, retry=5) + snode_api = snode.snode_client(timeout=5, retry=5) snode_api.spdk_process_kill(snode.rpc_port, snode.cluster_id) set_node_status(snode.get_id(), StorageNode.STATUS_OFFLINE) restart_lvs_port = snode.get_lvol_subsys_port(snode.lvstore) @@ -2796,7 +2798,7 @@ def shutdown_storage_node(node_id, force=False): time.sleep(1) logger.info("Stopping SPDK") try: - SNodeClient(snode.api_endpoint, timeout=10, retry=10).spdk_process_kill(snode.rpc_port, snode.cluster_id) + snode.snode_client(timeout=10, retry=10).spdk_process_kill(snode.rpc_port, snode.cluster_id) except SNodeClientException: logger.error('Failed to kill SPDK') return False @@ -2804,7 +2806,7 @@ def shutdown_storage_node(node_id, force=False): for dev in snode.nvme_devices: if dev.pcie_address not in pci_address: try: - ret = SNodeClient(snode.api_endpoint, timeout=30, retry=1).bind_device_to_nvme(dev.pcie_address) + ret = snode.snode_client(timeout=30, retry=1).bind_device_to_nvme(dev.pcie_address) logger.debug(ret) pci_address.append(dev.pcie_address) except Exception as e: @@ -3238,7 +3240,7 @@ def generate_automated_deployment_config(max_lvol, max_prov, sockets_to_use, nod return True -def deploy(ifname, isolate_cores=False): +def deploy(ifname, isolate_cores=False, access_token="access_token"): if not ifname: ifname = "eth0" @@ -3272,7 +3274,7 @@ def deploy(ifname, isolate_cores=False): logger.info(f"Node IP: {dev_ip}") scripts.configure_docker(dev_ip) - start_storage_node_api_container(dev_ip) + start_storage_node_api_container(dev_ip, access_token) if isolate_cores: utils.generate_realtime_variables_file(all_isolated_cores) @@ -3283,7 +3285,7 @@ def deploy(ifname, isolate_cores=False): return f"{dev_ip}:5000" -def start_storage_node_api_container(node_ip, cluster_ip=None): +def start_storage_node_api_container(node_ip, cluster_ip=None, access_token="access_token"): node_docker = docker.DockerClient(base_url=f"tcp://{node_ip}:2375", version="auto", timeout=60 * 5) # node_docker = docker.DockerClient(base_url='unix://var/run/docker.sock', version="auto", timeout=60 * 5) logger.info(f"Pulling image {constants.SIMPLY_BLOCK_DOCKER_IMAGE}") @@ -3297,7 +3299,8 @@ def start_storage_node_api_container(node_ip, cluster_ip=None): if cluster_ip is not None: log_config = LogConfig(type=LogConfig.types.GELF, config={"gelf-address": f"tcp://{cluster_ip}:12202"}) else: - log_config = LogConfig(type=LogConfig.types.JOURNALD) + pass + log_config = LogConfig(type=LogConfig.types.JOURNALD) node_docker.containers.run( constants.SIMPLY_BLOCK_DOCKER_IMAGE, @@ -3320,7 +3323,8 @@ def start_storage_node_api_container(node_ip, cluster_ip=None): f"DOCKER_IP={node_ip}", "WITHOUT_CLOUD_INFO=True", "SIMPLYBLOCK_LOG_LEVEL=DEBUG", - ] + f"SNODE_ACCESS_TOKEN={access_token}", + ], ) logger.info(f"Pulling image {constants.SIMPLY_BLOCK_SPDK_ULTRA_IMAGE}") pull_docker_image_with_retry(node_docker, constants.SIMPLY_BLOCK_SPDK_ULTRA_IMAGE) @@ -3436,7 +3440,7 @@ def health_check(node_id): try: logger.info("Connecting to node's API") - snode_api = SNodeClient(f"{snode.mgmt_ip}:5000") + snode_api = snode.snode_client() node_info, _ = snode_api.info() logger.info(f"Node info: {node_info['hostname']}") @@ -3453,7 +3457,7 @@ def get_info(node_id): logger.exception("Can not find storage node") return False - snode_api = SNodeClient(f"{snode.mgmt_ip}:5000") + snode_api = snode.snode_client() node_info, _ = snode_api.info() return json.dumps(node_info, indent=2) @@ -3752,7 +3756,7 @@ def recreate_lvstore_on_sec(secondary_node): logger.error(f"Failed to recover lvstore: {primary_node.lvstore} " f"on secondary: {secondary_node.get_id()}") storage_events.snode_restart_failed(secondary_node) - snode_api = SNodeClient(secondary_node.api_endpoint, timeout=5, retry=5) + snode_api = secondary_node.snode_client( timeout=5, retry=5) snode_api.spdk_process_kill(secondary_node.rpc_port, secondary_node.cluster_id) set_node_status(secondary_node.get_id(), StorageNode.STATUS_OFFLINE) return False @@ -3771,7 +3775,7 @@ def recreate_lvstore_on_sec(secondary_node): logger.error(f"Failed to recover BDev: {lv.lvol_uuid} " f"on secondary: {secondary_node.get_id()}") storage_events.snode_restart_failed(secondary_node) - snode_api = SNodeClient(secondary_node.api_endpoint, timeout=5, retry=5) + snode_api = secondary_node.snode_client(timeout=5, retry=5) snode_api.spdk_process_kill(secondary_node.rpc_port, secondary_node.cluster_id) set_node_status(secondary_node.get_id(), StorageNode.STATUS_OFFLINE) return False @@ -3966,7 +3970,7 @@ def recreate_lvstore(snode, force=False): def _kill_app(): storage_events.snode_restart_failed(snode) - snode_api = SNodeClient(snode.api_endpoint, timeout=5, retry=5) + snode_api = snode.snode_client(timeout=5, retry=5) snode_api.spdk_process_kill(snode.rpc_port, snode.cluster_id) set_node_status(snode.get_id(), StorageNode.STATUS_OFFLINE) diff --git a/simplyblock_web/api/v1/storage_node.py b/simplyblock_web/api/v1/storage_node.py index f0e88ed54..e2cf478f7 100644 --- a/simplyblock_web/api/v1/storage_node.py +++ b/simplyblock_web/api/v1/storage_node.py @@ -267,6 +267,10 @@ def storage_node_add(): elif isinstance(param, str): format_4k = param == "true" + access_token = None + if 'access_token' in req_data: + access_token = req_data['access_token'] + spdk_proxy_image = req_data.get('spdk_proxy_image', None) tasks_controller.add_node_add_task(cluster_id, { "cluster_id": cluster_id, @@ -286,6 +290,7 @@ def storage_node_add(): "ha_jm_count": ha_jm_count, "format_4k": format_4k, "spdk_proxy_image": spdk_proxy_image, + "access_token": access_token, }) return utils.get_response(True) diff --git a/simplyblock_web/api/v2/storage_node.py b/simplyblock_web/api/v2/storage_node.py index 0de5a280a..9bf5ff20a 100644 --- a/simplyblock_web/api/v2/storage_node.py +++ b/simplyblock_web/api/v2/storage_node.py @@ -53,6 +53,7 @@ class StorageNodeParams(BaseModel): ha_jm_count: int = Field(3) format_4k: bool = Field(False) spdk_proxy_image: Optional[str] = None + access_token: Optional[str] = None @api.post('/', name='clusters:storage-nodes:create', status_code=201, responses={201: {"content": None}}) @@ -81,6 +82,7 @@ def add(cluster: Cluster, parameters: StorageNodeParams): "ha_jm_count": parameters.ha_jm_count, "format_4k": parameters.format_4k, "spdk_proxy_image": parameters.spdk_proxy_image, + "access_token": parameters.access_token, } ) if not task_id_or_false: diff --git a/simplyblock_web/node_webapp.py b/simplyblock_web/node_webapp.py index 41f981397..d77b0f1be 100644 --- a/simplyblock_web/node_webapp.py +++ b/simplyblock_web/node_webapp.py @@ -1,22 +1,41 @@ #!/usr/bin/env python # encoding: utf-8 -from simplyblock_core import utils as core_utils import argparse +import os from flask_openapi3 import OpenAPI +from flask_httpauth import HTTPBasicAuth from simplyblock_core import constants +from simplyblock_core import utils as core_utils from simplyblock_web import utils from simplyblock_web.api import internal as internal_api logger = core_utils.get_logger(__name__) - -app = OpenAPI(__name__) +security_schemes = { + "basicAuth": { + "type": "http", + "scheme": "basic" + } +} +app = OpenAPI(__name__, security_schemes=security_schemes) app.url_map.strict_slashes = False app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True app.register_error_handler(Exception, utils.error_handler) +auth = HTTPBasicAuth() +app.basic_auth = auth +@auth.verify_password +def verify_password(username, password): + access_token = os.getenv("SNODE_ACCESS_TOKEN", "access_token") + if username == "token" and password == access_token: + return True + +@app.before_request +@auth.login_required +def before_request(): + pass @app.route('/', methods=['GET']) def status(): From a72130aa49c23da478fbd2b662026f9b0b4f7843 Mon Sep 17 00:00:00 2001 From: hamdykhader Date: Thu, 16 Apr 2026 00:29:16 +0300 Subject: [PATCH 2/4] Remove unused SNodeClient imports from multiple controllers --- .../controllers/device_controller.py | 1 - .../controllers/health_controller.py | 1 - .../controllers/lvol_controller.py | 1 - .../services/new_device_discovery.py | 56 ------------------- .../services/storage_node_monitor.py | 1 - simplyblock_web/node_webapp.py | 11 ++-- 6 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 simplyblock_core/services/new_device_discovery.py diff --git a/simplyblock_core/controllers/device_controller.py b/simplyblock_core/controllers/device_controller.py index 1aa734d64..f4e70a130 100644 --- a/simplyblock_core/controllers/device_controller.py +++ b/simplyblock_core/controllers/device_controller.py @@ -9,7 +9,6 @@ from simplyblock_core.models.storage_node import StorageNode from simplyblock_core.prom_client import PromClient from simplyblock_core.rpc_client import RPCClient -from simplyblock_core.snode_client import SNodeClient logger = logging.getLogger() diff --git a/simplyblock_core/controllers/health_controller.py b/simplyblock_core/controllers/health_controller.py index c636de9bf..cc33857e2 100644 --- a/simplyblock_core/controllers/health_controller.py +++ b/simplyblock_core/controllers/health_controller.py @@ -12,7 +12,6 @@ from simplyblock_core.models.nvme_device import NVMeDevice, JMDevice, RemoteDevice from simplyblock_core.models.storage_node import StorageNode from simplyblock_core.rpc_client import RPCClient -from simplyblock_core.snode_client import SNodeClient from simplyblock_core.controllers import device_controller logger = utils.get_logger(__name__) diff --git a/simplyblock_core/controllers/lvol_controller.py b/simplyblock_core/controllers/lvol_controller.py index ee158e48d..482cd9a99 100644 --- a/simplyblock_core/controllers/lvol_controller.py +++ b/simplyblock_core/controllers/lvol_controller.py @@ -21,7 +21,6 @@ from simplyblock_core.models.storage_node import StorageNode from simplyblock_core.prom_client import PromClient from simplyblock_core.rpc_client import RPCClient -from simplyblock_core.snode_client import SNodeClient logger = lg.getLogger() diff --git a/simplyblock_core/services/new_device_discovery.py b/simplyblock_core/services/new_device_discovery.py deleted file mode 100644 index b96fdf8ea..000000000 --- a/simplyblock_core/services/new_device_discovery.py +++ /dev/null @@ -1,56 +0,0 @@ -# coding=utf-8 -import time - -from simplyblock_core import constants, db_controller, storage_node_ops, utils -from simplyblock_core.models.nvme_device import NVMeDevice -from simplyblock_core.models.storage_node import StorageNode - -from simplyblock_core.snode_client import SNodeClient - -logger = utils.get_logger(__name__) - -# get DB controller -db = db_controller.DBController() - - -logger.info("Starting new device discovery service...") -while True: - nodes = db.get_storage_nodes() - for node in nodes: - time.sleep(constants.DEV_DISCOVERY_INTERVAL_SEC) - break - auto_restart_devices = [] - online_devices = [] - if node.status != StorageNode.STATUS_ONLINE or node.is_secondary_node: # pass - logger.warning(f"Skipping node, id: {node.get_id()}, status: {node.status}") - continue - - known_sn = [dev.serial_number for dev in node.nvme_devices] - if node.jm_device and 'serial_number' in node.jm_device.device_data_dict: - known_sn.append(node.jm_device.device_data_dict['serial_number']) - - snode_api = SNodeClient(node.api_endpoint) - node_info, _ = snode_api.info() - - - # check for unused nvme devices - if "lsblk" in node_info: - for dev in node_info['nvme_devices']: - if dev['serial_number'] in known_sn: - continue - for block_dev in node_info['lsblk']['blockdevices']: - if block_dev['name'] == dev['device_name']: - if 'children' not in block_dev: - logger.info(f"Unused device found: {dev['address']}") - # try mount to spdk driver - snode_api.bind_device_to_spdk(dev['address']) - time.sleep(3) - devs = storage_node_ops.addNvmeDevices(node, [dev['address']]) - if devs: - logger.info(f"New ssd found: {dev['address']}") - new_dev = devs[0] - new_dev.status = NVMeDevice.STATUS_NEW - node.nvme_devices.append(new_dev) - node.write_to_db(db.kv_store) - - time.sleep(constants.DEV_DISCOVERY_INTERVAL_SEC) diff --git a/simplyblock_core/services/storage_node_monitor.py b/simplyblock_core/services/storage_node_monitor.py index 008cd5f37..29faa1700 100644 --- a/simplyblock_core/services/storage_node_monitor.py +++ b/simplyblock_core/services/storage_node_monitor.py @@ -10,7 +10,6 @@ from simplyblock_core.models.job_schedule import JobSchedule from simplyblock_core.models.nvme_device import NVMeDevice, JMDevice from simplyblock_core.models.storage_node import StorageNode -from simplyblock_core.snode_client import SNodeClient logger = utils.get_logger(__name__) diff --git a/simplyblock_web/node_webapp.py b/simplyblock_web/node_webapp.py index d77b0f1be..5e6010baa 100644 --- a/simplyblock_web/node_webapp.py +++ b/simplyblock_web/node_webapp.py @@ -3,7 +3,7 @@ import argparse import os -from flask_openapi3 import OpenAPI +from flask_openapi3 import OpenAPI, SecurityScheme from flask_httpauth import HTTPBasicAuth from simplyblock_core import constants @@ -14,10 +14,11 @@ logger = core_utils.get_logger(__name__) security_schemes = { - "basicAuth": { - "type": "http", - "scheme": "basic" - } + "basicAuth": SecurityScheme( + type="http", + scheme="basic", + description="Standard HTTP Basic Authentication" + ) } app = OpenAPI(__name__, security_schemes=security_schemes) app.url_map.strict_slashes = False From 4a0171af978c55a5424fa608c03487ac3b0fb74d Mon Sep 17 00:00:00 2001 From: hamdykhader Date: Thu, 16 Apr 2026 00:37:03 +0300 Subject: [PATCH 3/4] Add type ignore for OpenAPI security schemes in node_webapp.py --- simplyblock_web/node_webapp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/simplyblock_web/node_webapp.py b/simplyblock_web/node_webapp.py index 5e6010baa..3b6f03dda 100644 --- a/simplyblock_web/node_webapp.py +++ b/simplyblock_web/node_webapp.py @@ -20,13 +20,12 @@ description="Standard HTTP Basic Authentication" ) } -app = OpenAPI(__name__, security_schemes=security_schemes) +app = OpenAPI(__name__, security_schemes=security_schemes) # type: ignore[arg-type] app.url_map.strict_slashes = False app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True app.register_error_handler(Exception, utils.error_handler) auth = HTTPBasicAuth() -app.basic_auth = auth @auth.verify_password def verify_password(username, password): access_token = os.getenv("SNODE_ACCESS_TOKEN", "access_token") From b6f170234bcffb07f3d85f91d0ee36ded9f6f8cf Mon Sep 17 00:00:00 2001 From: hamdykhader Date: Thu, 16 Apr 2026 00:59:09 +0300 Subject: [PATCH 4/4] Update deploy function to generate the access_token if not provided --- simplyblock_core/storage_node_ops.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/simplyblock_core/storage_node_ops.py b/simplyblock_core/storage_node_ops.py index 56dfceb82..4548de84f 100755 --- a/simplyblock_core/storage_node_ops.py +++ b/simplyblock_core/storage_node_ops.py @@ -3240,7 +3240,7 @@ def generate_automated_deployment_config(max_lvol, max_prov, sockets_to_use, nod return True -def deploy(ifname, isolate_cores=False, access_token="access_token"): +def deploy(ifname, isolate_cores=False, access_token=None): if not ifname: ifname = "eth0" @@ -3274,6 +3274,9 @@ def deploy(ifname, isolate_cores=False, access_token="access_token"): logger.info(f"Node IP: {dev_ip}") scripts.configure_docker(dev_ip) + if not access_token: + access_token = utils.generate_string(32) + start_storage_node_api_container(dev_ip, access_token) if isolate_cores: