Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ dependencies = [
"graypy==2.1.0",
"boto3",
"jinja2",
"jc",
"prometheus-client",
"ec2-metadata",
"flask-swagger-ui",
Expand Down
2 changes: 1 addition & 1 deletion simplyblock_core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ When adding a new `SecretStr` field to an existing model, grep for `json.dumps`,

## Client Pattern (RPC, SNode, Firewall API)

Clients in `rpc_client.py`, `snode_client.py`, and `fw_api_client.py` accept `SecretStr` parameters and follow the **log-then-unwrap** pattern:
Clients in `rpc_client.py` and `snode_client.py` accept `SecretStr` parameters and follow the **log-then-unwrap** pattern:

1. Log the payload dict containing `SecretStr` wrappers (masked by Pydantic's `__repr__`).
2. Call `unwrap_secrets_for_send(payload)` from `utils/secrets.py` to produce a plaintext dict.
Expand Down
6 changes: 3 additions & 3 deletions simplyblock_core/cluster_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
from simplyblock_core.release_upgrades import jc_compression_upgrade
from simplyblock_core.rpc_client import RPCException
from simplyblock_core.settings import Settings
from simplyblock_core.utils import port_block, pull_docker_image_with_retry
from simplyblock_core.utils import pull_docker_image_with_retry

logger = utils.get_logger(__name__)

Expand Down Expand Up @@ -1645,7 +1645,7 @@ def _recreate_non_leader_lvs(node_id) -> bool:
if not is_fresh_activation and primary_node.status == StorageNode.STATUS_ONLINE:
try:
leader_port = primary_node.get_lvol_subsys_port(primary_node.lvstore)
port_block.set_port(primary_node, leader_port, block=True, timeout=3, retry=1)
primary_node.rpc_client(timeout=3, retry=1).nvmf_port_block(leader_port)
tcp_ports_events.port_deny(primary_node, leader_port)
leader_blocked = True
time.sleep(0.5)
Expand All @@ -1668,7 +1668,7 @@ def _recreate_non_leader_lvs(node_id) -> bool:
finally:
if leader_blocked:
try:
port_block.set_port(primary_node, leader_port, block=False, timeout=3, retry=1)
primary_node.rpc_client(timeout=3, retry=1).nvmf_port_unblock(leader_port)
tcp_ports_events.port_allowed(primary_node, leader_port)
except Exception as ue:
logger.error(
Expand Down
1 change: 0 additions & 1 deletion simplyblock_core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,6 @@ def get_config_var(name, default=None):

# Backward compatibility aliases
RPC_PORT_RANGE_START = RPC_BASE_PORT
FW_PORT_START = SNODE_API_PORT
LVOL_NVMF_PORT_START = NVMF_BASE_PORT
NODE_NVMF_PORT_START = NVMF_BASE_PORT
NODE_HUBLVOL_PORT_START = NVMF_BASE_PORT
Expand Down
13 changes: 4 additions & 9 deletions simplyblock_core/controllers/health_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,23 +251,18 @@ def _log_port_check_failure(db_controller, snode, port, exc):


def check_port_on_node(snode, port_id):
from simplyblock_core.utils import port_block
return not port_block.is_port_blocked(snode, port_id)
return int(port_id) not in snode.rpc_client(timeout=5, retry=5).nvmf_get_blocked_ports()


def check_ports_on_node(snode, port_ids):
"""Batch variant of :func:`check_port_on_node`: ONE
``nvmf_get_blocked_ports`` fetch answers every port in ``port_ids``.
Returns ``{port: bool}`` (True = open). Falls back to per-port checks on
legacy nodes without the RPC. Raises on fetch failure — callers treat it
like the single-port failure path."""
from simplyblock_core.utils import port_block
Returns ``{port: bool}`` (True = open). Raises on fetch failure — callers
treat it like the single-port failure path."""
port_ids = list(port_ids)
if not port_ids:
return {}
blocked = port_block.get_blocked_ports_set(snode)
if blocked is None:
return {p: check_port_on_node(snode, p) for p in port_ids}
blocked = snode.rpc_client(timeout=5, retry=5).nvmf_get_blocked_ports()
return {p: int(p) not in blocked for p in port_ids}


Expand Down
100 changes: 0 additions & 100 deletions simplyblock_core/fw_api_client.py

This file was deleted.

3 changes: 0 additions & 3 deletions simplyblock_core/models/storage_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,9 @@ class StorageNode(BaseNodeObject):
active_tcp: bool = True
active_rdma: bool = False
socket: int = 0
firewall_port: int = 5001
lvol_poller_mask: str = ""
spdk_proxy_image: str = ""
transfer_hublvol: HubLVol = None # type: ignore[assignment]
# spdk image tag
spdk_version: str = ""

def get_lvol_subsys_port(self, lvs_name=None):
"""Get the client-facing NVMeoF port for a specific lvstore.
Expand Down
27 changes: 25 additions & 2 deletions simplyblock_core/rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,19 @@ def jc_compression_start(self, jm_vuid):
return self._request("jc_compression", jm_vuid=jm_vuid)

def nvmf_port_block(self, port, is_reject=False):
"""Fence ``port``: SPDK stops serving client connections on it.

Callers fencing a peer's client listener build this client with a
short budget, e.g. ``node.rpc_client(timeout=0.5, retry=1)``.
``timeout`` is per HTTP attempt and the client retries ``retry``
times with backoff, so the wall clock is roughly (retry+1) x timeout
plus backoff. While the port is blocked the peer cannot answer
keep-alives, and the client KATO is 4s: a fence window longer than
that is an outage. On 2026-08-31 a failing block on a rebooting peer
with a (5s, 2) budget took 12s, left a healthy survivor fenced, and
the client lost its last path at 4.1s. A healthy peer answers in
~11ms, so 0.5s is ~45x the observed latency.
"""
params = {"port": port}
if is_reject:
params["reject"] = is_reject
Expand All @@ -2005,8 +2018,18 @@ def nvmf_port_block(self, port, is_reject=False):
def nvmf_port_unblock(self, port):
return self._request("nvmf_port_unblock", port=port)

def nvmf_get_blocked_ports(self):
return self._request("nvmf_get_blocked_ports")
def nvmf_get_blocked_ports(self) -> set[int]:
"""Ports currently fenced on this SPDK.

The RPC returns ``{"blocked_ports": [{"port": N, ...}, ...]}``. Test
membership against the projected set, never against the raw reply:
``port in reply`` tested the dict's keys and read every fenced port as
open (2026-09-11, k8s rapid failover)."""
blocked = self._request("nvmf_get_blocked_ports")
if not blocked:
return set()
entries = blocked.get("blocked_ports", []) if isinstance(blocked, dict) else []
return {int(e.get("port", -1)) for e in entries}

def bdev_raid_get_bdevs(self):
return self._request("bdev_raid_get_bdevs", category="online")
Expand Down
3 changes: 1 addition & 2 deletions simplyblock_core/services/storage_node_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1869,8 +1869,7 @@ def _remediate_stale_port_blocks(db, snode, port_results, port_lvs_owner):
"nothing released it. Unblocking.",
port, snode.get_id(), held, snode.status, owner.lvstore)
try:
from simplyblock_core.utils import port_block
port_block.set_port(snode, port, block=False, timeout=5, retry=1)
snode.rpc_client(timeout=5, retry=1).nvmf_port_unblock(port)
_blocked_port_since.pop(key, None)
except Exception as e:
logger.error("Failed to unblock stale port %s on %s: %s",
Expand Down
3 changes: 1 addition & 2 deletions simplyblock_core/services/tasks_runner_port_allow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
serve,
set_result,
)
from simplyblock_core.utils import port_block

logger = utils.get_logger(__name__)

Expand Down Expand Up @@ -971,7 +970,7 @@ def exec_port_allow_task(task):
lvs_ports.append(port_number)
logger.info(f"Allow LVS ports {lvs_ports} on node {node.get_id()}")
for p in lvs_ports:
port_block.set_port(node, p, block=False, timeout=5, retry=2)
node.rpc_client(timeout=5, retry=2).nvmf_port_unblock(p)
tcp_ports_events.port_allowed(node, p)

# Deferred ANA failover, AFTER the port release: a secondary recovering
Expand Down
4 changes: 1 addition & 3 deletions simplyblock_core/snode_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def recalculate_cores_distribution(self, cores, number_of_alceml_devices):
def spdk_process_start(self, l_cores, spdk_mem, spdk_image=None, spdk_debug=None, cluster_ip=None,
fdb_connection: SecretStr | None = None, namespace=None, server_ip=None, rpc_port=None,
rpc_username=None, rpc_password: SecretStr | None = None, multi_threading_enabled=False, timeout=0, ssd_pcie=None,
total_mem=None, system_mem=None, cluster_mode=None, socket=0, firewall_port=0, cluster_id=None,
total_mem=None, system_mem=None, cluster_mode=None, socket=0, cluster_id=None,
spdk_proxy_image=None, mcp_max_unavailable=None):
params = {
"cluster_ip": cluster_ip,
Expand Down Expand Up @@ -162,8 +162,6 @@ def spdk_process_start(self, l_cores, spdk_mem, spdk_image=None, spdk_debug=None
params["system_mem"] = system_mem
if cluster_mode:
params["cluster_mode"] = cluster_mode
if firewall_port:
params["firewall_port"] = firewall_port
params["socket"] = socket
if cluster_id:
params["cluster_id"] = cluster_id
Expand Down
23 changes: 10 additions & 13 deletions simplyblock_core/storage_node_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@
dial_backoff,
hublvol_reconnect,
nvmf_listener,
port_block,
pull_docker_image_with_retry,
rpc_budget,
)
Expand Down Expand Up @@ -4280,7 +4279,6 @@ 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.spdk_version = spdk_proxy_image.split(":")[1]

if 'cpu_count' in node_info:
snode.cpu = node_info['cpu_count']
Expand Down Expand Up @@ -8579,7 +8577,6 @@ def _restart_storage_node_impl(

if spdk_proxy_image:
snode.spdk_proxy_image = spdk_proxy_image
snode.spdk_version = spdk_proxy_image.split(":")[1]

if not snode.spdk_proxy_image:
snode.spdk_proxy_image = cluster.container_image_prefix + constants.SIMPLY_BLOCK_DOCKER_IMAGE
Expand Down Expand Up @@ -9380,7 +9377,7 @@ def _fenced_secondary_of(snode: StorageNode, not_online_nodes):
continue
port = sec.get_lvol_subsys_port(snode.lvstore)
try:
blocked = port_block.get_blocked_ports_set(sec, timeout=5, retry=1)
blocked = sec.rpc_client(timeout=5, retry=1).nvmf_get_blocked_ports()
except (RPCException, SNodeClientException, OSError) as e:
logger.warning("Cannot read blocked ports of secondary %s of %s: %s",
sec.get_id(), snode.get_id(), e)
Expand Down Expand Up @@ -12725,7 +12722,7 @@ def _abort_and_unblock(reason):
leader_node, leader_lvs_port, primary_node.jm_vuid, db_controller,
"Restart abort"):
try:
port_block.set_port(leader_node, leader_lvs_port, block=False, timeout=0.5, retry=2)
leader_node.rpc_client(timeout=0.5, retry=2).nvmf_port_unblock(leader_lvs_port)
_deferred_port_events.append(("allow", leader_node, leader_lvs_port))
_mark_leader_unblocked()
except Exception as ue:
Expand Down Expand Up @@ -12777,7 +12774,7 @@ def _abort_and_unblock(reason):
attempts = 3
for attempt in range(1, attempts + 1):
try:
port_block.set_port(leader_node, leader_lvs_port, block=True, timeout=3, retry=1)
leader_node.rpc_client(timeout=3, retry=1).nvmf_port_block(leader_lvs_port)
_deferred_port_events.append(("deny", leader_node, leader_lvs_port))
leader_port_blocked = True
_mark_leader_blocked()
Expand Down Expand Up @@ -13086,7 +13083,7 @@ def _lvol_bdev_registered(lv):
leader_node, leader_lvs_port, primary_node.jm_vuid, db_controller,
"Attach-retry gap"):
try:
port_block.set_port(leader_node, leader_lvs_port, block=False, timeout=3, retry=1)
leader_node.rpc_client(timeout=3, retry=1).nvmf_port_unblock(leader_lvs_port)
_deferred_port_events.append(("allow", leader_node, leader_lvs_port))
leader_port_blocked = False
_mark_leader_unblocked()
Expand All @@ -13096,7 +13093,7 @@ def _lvol_bdev_registered(lv):
"failed: %s", leader_node.get_id(), ue)
time.sleep(ATTACH_RETRY_GAP_SEC)
try:
port_block.set_port(leader_node, leader_lvs_port, block=True, timeout=3, retry=1)
leader_node.rpc_client(timeout=3, retry=1).nvmf_port_block(leader_lvs_port)
_deferred_port_events.append(("deny", leader_node, leader_lvs_port))
leader_port_blocked = True
_mark_leader_blocked()
Expand Down Expand Up @@ -13128,7 +13125,7 @@ def _lvol_bdev_registered(lv):
attempts = 3
for attempt in range(1, attempts + 1):
try:
port_block.set_port(leader_node, leader_lvs_port, block=False, timeout=3, retry=1)
leader_node.rpc_client(timeout=3, retry=1).nvmf_port_unblock(leader_lvs_port)
_deferred_port_events.append(("allow", leader_node, leader_lvs_port))
unblocked = True
_mark_leader_unblocked()
Expand Down Expand Up @@ -13315,7 +13312,7 @@ def _release_lvs_subsys_port_on_peers(lvs_node, exclude_node_id, db_controller):
"Defensive unblock after failed recreate of %s"
% lvs_node.lvstore):
continue
port_block.set_port(peer, port, block=False, timeout=0.5, retry=2)
peer.rpc_client(timeout=0.5, retry=2).nvmf_port_unblock(port)
tcp_ports_events.port_allowed(peer, port)
logger.info("Defensive unblock: allowed LVS port %s on peer %s after "
"failed recreate of %s", port, pid, lvs_node.lvstore)
Expand Down Expand Up @@ -13861,7 +13858,7 @@ def _unblock_peer_port(peer):
pass
return
try:
port_block.set_port(peer, snode_lvs_port, block=False, timeout=0.5, retry=2)
peer.rpc_client(timeout=0.5, retry=2).nvmf_port_unblock(snode_lvs_port)
_deferred_port_events.append(("allow", peer, snode_lvs_port))
_t0 = _block_started.pop(peer.get_id(), None)
if _t0 is not None:
Expand Down Expand Up @@ -14226,7 +14223,7 @@ def _fenced(method, *args, budget=None, **kwargs):
if sec_node in blocked_peers:
continue
try:
port_block.set_port(sec_node, snode_lvs_port, block=True, timeout=0.5, retry=1)
sec_node.rpc_client(timeout=0.5, retry=1).nvmf_port_block(snode_lvs_port)
_deferred_port_events.append(("deny", sec_node, snode_lvs_port))
blocked_peers.append(sec_node)
_block_started[sec_node.get_id()] = time.monotonic()
Expand Down Expand Up @@ -14304,7 +14301,7 @@ def _fenced(method, *args, budget=None, **kwargs):
db_controller.atomic_update(
current_leader,
lambda x: setattr(x, "lvstore_status", "in_creation"))
port_block.set_port(current_leader, snode_lvs_port, block=True, timeout=0.5, retry=1)
current_leader.rpc_client(timeout=0.5, retry=1).nvmf_port_block(snode_lvs_port)
_deferred_port_events.append(("deny", current_leader, snode_lvs_port))
blocked_peers.append(current_leader)
_block_started[current_leader.get_id()] = time.monotonic()
Expand Down
Loading
Loading