Skip to content

fix(port-block): drop the FirewallClient fallback and the firewall-port plumbing - #1497

Merged
wmousa merged 3 commits into
mainfrom
chore/remove-firewall-leftovers
Oct 17, 2026
Merged

wmousa merged 3 commits into
mainfrom
chore/remove-firewall-leftovers

Conversation

@wmousa

@wmousa wmousa commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Removes the firewall/iptables leftovers from the control plane on main. Three commits:

1. Fence only through the SPDK RPC, drop the FirewallClient fallback. This is the main version of #1496, which targets R26.3, so main doesn't need a separate cherry-pick of #1496.

2. Remove the dead plumbing behind it.

  • firewall_port → FW_PORT:

    • the StorageNode.firewall_port field;
    • the spdk_process_start(firewall_port=) param in SNodeClient;
    • the field in both node-API SPDKParams;
    • the FW_PORT env on the SPDK container (docker env and the k8s template);
    • constants.FW_PORT_START.

    The SPDK image stopped reading FW_PORT with ultra Testing K8s e2e run. Adding check for docker logs if it is not k8s #218.

  • utils.get_next_fw_port: nothing called it. Its tests are removed.

  • Deprecated node API routes /snode/firewall_set_port and /snode/get_firewall: they already returned False and did nothing, but were still routes on the unauthenticated :5000 API.

  • StorageNode.spdk_version: it was only read by the fallback removed in commit 1.

  • The jc dependency (it parsed iptables output). uv.lock was regenerated with uv 0.12.22, the version docker/Dockerfile pins, which also drops ruamel-yaml and xmltodict. That uv writes revision = 5, and uv lock --check passes.

Removing a field from the hand-rolled BaseModel is safe for existing FDB records: from_dict only reads the model's declared attributes, so the stored firewall_port / spdk_version keys are ignored.

Not changed

  • --snode-api-port / cluster.snode_api_port: its only reader was get_next_fw_port, so it no longer has any effect. It's part of the cluster-create CLI and the v1/v2 API, so removing it should be a separate interface change.
  • install_deps.sh still installs iptables on Docker-mode hosts. Docker needs it.

Test plan

  • ruff check: clean
  • mypy simplyblock_web simplyblock_cli simplyblock_core: only the 2 asyncio.open_unix_connection errors that also occur on unmodified main when run on Windows
  • Unit suite: 4054 passed. The 34 failures (31 in test_spdk_proxy_unit.py, 3 in test_lblk_persistent_names.py) are in files this PR does not touch. They look Windows-specific (the proxy tests use Unix sockets) and should pass on Linux CI.
  • uv lock --check
  • Integration tier (needs FDB): CI
  • On a cluster: SPDK pod spec has no FW_PORT; fence and unfence work through nvmf_port_block

3. Remove utils.port_block (Max's review on #1496). With the fallback gone, the module was a pass-through:

  • set_port: its 13 call sites all passed block= as a constant. Each now calls node.rpc_client(timeout=…, retry=…).nvmf_port_block(port) or .nvmf_port_unblock(port) directly, with the same budget it used before. The note on why fences need a short budget moved to RPCClient.nvmf_port_block.
  • get_blocked_ports_set: the conversion to a set of port numbers moved into RPCClient.nvmf_get_blocked_ports, its only caller.
  • is_port_blocked: removed; check_port_on_node does the check inline.
  • Tests: they faked fencing by patching port_block.set_port, and that patch also told them which node each fence was for. The new tests._mocks.route_fences keeps that: it wraps each node's rpc_client so nvmf_port_block / nvmf_port_unblock reach the same (node, port, block) callback as before. In addition:
    • Source-text checks and "must not fence" checks now look for the RPC calls.
    • Mocks of nvmf_get_blocked_ports return a set.
    • The expansion simulator returns a set too.
  • Local verification: the affected integration files can run with mocks, using the unit-tier FDB stub and --noconftest. That gives the same result before and after this commit: 302 passed, with the same environment-only failures. ftt2 and expansion_sim need their real fixtures, so CI is the check for those.

Found while doing this, not changed: the l2-fw / m2-fw cases in tests/integration/ftt2/test_restart_peer_states.py disconnect a peer when its mock server sees a firewall_set_port RPC. Fencing never reached the mock server, before or after this PR, and the server has no port RPCs, so those cases never trigger and just repeat their siblings. Making them real means adding nvmf_port_block to FTT2MockRpcServer and hooking that instead.

🤖 Generated with Claude Code

wmousa and others added 2 commits October 16, 2026 18:00
…ient fallback

port_block.set_port sent fences to the legacy firewall webapp
(FirewallClient, mgmt_ip:firewall_port) whenever node.spdk_version was
empty, and redirected a method-not-found from nvmf_port_block there too.
25.10.x node records never carried spdk_version, and it is only set by a
restart that passes --spdk-proxy-image, so a node upgraded to 26.3 kept
"" and every fence on it went to a port nothing listens on: the SPDK
image no longer ships the webapp or iptables (ultra #218). The fence
failed with connection refused.

- port_block: set_port / get_blocked_ports_set / is_port_blocked use the
  SPDK RPCs only; an RPC error, method-not-found included, propagates.
- health_controller.check_ports_on_node: drop the per-port legacy branch.
- Delete fw_api_client.py; port_block was its only user.
- Tests: the ftt2 and dual-FT fixtures patch port_block instead of
  FirewallClient; the expansion simulator answers nvmf_port_block /
  nvmf_port_unblock / nvmf_get_blocked_ports instead of stubbing
  FirewallClient; regression tests in test_port_block_rpc_only.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing has read any of this since fencing moved to the SPDK
nvmf_port_block RPC and the SPDK image dropped iptables and the firewall
webapp (ultra #218):

- StorageNode.firewall_port, the spdk_process_start(firewall_port=) param,
  the field in both node-API SPDKParams, the FW_PORT env on the SPDK
  container (docker and the k8s template) and constants.FW_PORT_START.
- utils.get_next_fw_port: no callers.
- The deprecated node-API routes /snode/firewall_set_port and
  /snode/get_firewall, which only returned False.
- StorageNode.spdk_version: only the removed FirewallClient fallback
  read it.
- The jc dependency (it parsed iptables output); uv.lock regenerated
  with uv 0.12.22, the version docker/Dockerfile pins.

Existing FDB records keep their stored firewall_port / spdk_version keys;
BaseModel.from_dict only reads declared attributes, so they are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the target/main Targets the development branch label Oct 16, 2026
@Hamdy-khader
Hamdy-khader requested a review from mxsrc October 16, 2026 16:35

@mxsrc mxsrc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, I'd only raise the same comment as on #1496 though.

With the FirewallClient fallback gone, port_block was a pass-through
(review on #1496):

- set_port only chose nvmf_port_block or nvmf_port_unblock, and every
  caller passed block= as a constant: the 13 call sites now call
  node.rpc_client(timeout=.., retry=..).nvmf_port_block/unblock(port)
  with the budget each already used. The rationale for the short fence
  budget moves to RPCClient.nvmf_port_block.
- get_blocked_ports_set was the only caller of
  RPCClient.nvmf_get_blocked_ports: the projection to a set of port
  numbers moves into that method.
- is_port_blocked was a one-line membership test; check_port_on_node
  does it inline.

Tests: they faked fencing by patching port_block.set_port, which also
told them which node a fence was for. tests._mocks.route_fences keeps
that: it wraps each node's rpc_client so nvmf_port_block/unblock reach
the same (node, port, block) callback the tests already used. Source
text anchors and "must not fence" assertions now look for the RPC
calls, and mocks of nvmf_get_blocked_ports return a set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wmousa
wmousa merged commit f5a23e9 into main Oct 17, 2026
21 checks passed
@wmousa
wmousa deleted the chore/remove-firewall-leftovers branch October 17, 2026 06:58
wmousa added a commit that referenced this pull request Oct 17, 2026
Nothing has read any of this since fencing moved to the SPDK
nvmf_port_block RPC and the SPDK image dropped iptables and the firewall
webapp (ultra #218):

- StorageNode.firewall_port, the spdk_process_start(firewall_port=) param,
  the field in both node-API SPDKParams, the FW_PORT env on the SPDK
  container (docker and the k8s template) and constants.FW_PORT_START.
- utils.get_next_fw_port: no callers.
- The deprecated node-API routes /snode/firewall_set_port and
  /snode/get_firewall, which only returned False.
- StorageNode.spdk_version: only the removed FirewallClient fallback
  read it.
- The jc dependency (it parsed iptables output), from requirements.txt.

Existing FDB records keep their stored firewall_port / spdk_version keys;
BaseModel.from_dict only reads declared attributes, so they are ignored.

(cherry picked from commit d3459b9, part of #1497)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wmousa added a commit that referenced this pull request Oct 17, 2026
With the FirewallClient fallback gone, port_block was a pass-through
(review on #1496):

- set_port only chose nvmf_port_block or nvmf_port_unblock, and every
  caller passed block= as a constant: the 13 call sites now call
  node.rpc_client(timeout=.., retry=..).nvmf_port_block/unblock(port)
  with the budget each already used. The rationale for the short fence
  budget moves to RPCClient.nvmf_port_block.
- get_blocked_ports_set was the only caller of
  RPCClient.nvmf_get_blocked_ports: the projection to a set of port
  numbers moves into that method.
- is_port_blocked was a one-line membership test; check_port_on_node
  does it inline.

Tests: they faked fencing by patching port_block.set_port, which also
told them which node a fence was for. tests._mocks.route_fences keeps
that: it wraps each node's rpc_client so nvmf_port_block/unblock reach
the same (node, port, block) callback the tests already used. Source
text anchors and "must not fence" assertions now look for the RPC
calls, and mocks of nvmf_get_blocked_ports return a set.

R26.3 port of 59a238b (part of #1497): the same edits replayed on
R26.3, where RPCClient calls _request3 and the two-node and
failback-listener tests do not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wmousa added a commit that referenced this pull request Oct 17, 2026
…ient fallback (R26.3) (#1496)

* Revert "fix(spdk-start): pass the host's fence ports to the SPDK pod so the entrypoint stops flushing iptables (#1484)"

This reverts commit 88a50e2. The SPDK image no longer touches iptables
at all (ultra #218): 25.10.x -> 26.3 upgrades are offline, so no 25.10
fence rules need cleaning at SPDK start, and nothing reads FENCE_PORTS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(port-block): fence only through the SPDK RPC, drop the FirewallClient fallback

port_block.set_port sent fences to the legacy firewall webapp
(FirewallClient, mgmt_ip:firewall_port) whenever node.spdk_version was
empty, and redirected a method-not-found from nvmf_port_block there too.
25.10.x node records never carried spdk_version, and it is only set by a
restart that passes --spdk-proxy-image, so a node upgraded to 26.3 kept
"" and every fence on it went to a port nothing listens on: the SPDK
image no longer ships the webapp or iptables (ultra #218). The fence
failed with connection refused.

- port_block: set_port / get_blocked_ports_set / is_port_blocked use the
  SPDK RPCs only; an RPC error, method-not-found included, propagates.
- health_controller.check_ports_on_node: drop the per-port legacy branch.
- Delete fw_api_client.py; port_block was its only user.
- Tests: the ftt2 and dual-FT fixtures patch port_block instead of
  FirewallClient; the expansion simulator answers nvmf_port_block /
  nvmf_port_unblock / nvmf_get_blocked_ports instead of stubbing
  FirewallClient; regression tests in test_port_block_rpc_only.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* lint: annotate TTLCache._instances as ClassVar (RUF012)

The Linter job installs an unpinned ruff; 0.17.1 flags the class-level
WeakSet registry as a mutable class attribute. It is meant to be shared
across instances, so ClassVar states that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: remove the firewall-port plumbing and the jc dependency

Nothing has read any of this since fencing moved to the SPDK
nvmf_port_block RPC and the SPDK image dropped iptables and the firewall
webapp (ultra #218):

- StorageNode.firewall_port, the spdk_process_start(firewall_port=) param,
  the field in both node-API SPDKParams, the FW_PORT env on the SPDK
  container (docker and the k8s template) and constants.FW_PORT_START.
- utils.get_next_fw_port: no callers.
- The deprecated node-API routes /snode/firewall_set_port and
  /snode/get_firewall, which only returned False.
- StorageNode.spdk_version: only the removed FirewallClient fallback
  read it.
- The jc dependency (it parsed iptables output), from requirements.txt.

Existing FDB records keep their stored firewall_port / spdk_version keys;
BaseModel.from_dict only reads declared attributes, so they are ignored.

(cherry picked from commit d3459b9, part of #1497)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: remove utils.port_block, fence through the RPC client directly

With the FirewallClient fallback gone, port_block was a pass-through
(review on #1496):

- set_port only chose nvmf_port_block or nvmf_port_unblock, and every
  caller passed block= as a constant: the 13 call sites now call
  node.rpc_client(timeout=.., retry=..).nvmf_port_block/unblock(port)
  with the budget each already used. The rationale for the short fence
  budget moves to RPCClient.nvmf_port_block.
- get_blocked_ports_set was the only caller of
  RPCClient.nvmf_get_blocked_ports: the projection to a set of port
  numbers moves into that method.
- is_port_blocked was a one-line membership test; check_port_on_node
  does it inline.

Tests: they faked fencing by patching port_block.set_port, which also
told them which node a fence was for. tests._mocks.route_fences keeps
that: it wraps each node's rpc_client so nvmf_port_block/unblock reach
the same (node, port, block) callback the tests already used. Source
text anchors and "must not fence" assertions now look for the RPC
calls, and mocks of nvmf_get_blocked_ports return a set.

R26.3 port of 59a238b (part of #1497): the same edits replayed on
R26.3, where RPCClient calls _request3 and the two-node and
failback-listener tests do not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
BombermaG added a commit that referenced this pull request Oct 20, 2026
Brings in main up to ceff9f5: two-node HA with the arbiter (#1466) and
its campaign fixes, regular-cluster gating of the two-node paths (#1475),
activation / ANA failback (#1453), the ANA failover retry (#1480), the
lvstore poller on nvmf cores (#1452), port fences through
nvmf_port_block / nvmf_port_unblock without FirewallClient (#1497), the
fenced bdev_lvol_set_leader budget (#1495), keep-alive 2 s on arbitrated
clusters (#1494), the unfenced RPC bound during a removal (#1477) and
replica repair (#1463).

Sync replication adapted to it:
- the leadership hand-off fence (_LvsFence, _fence_and_demote_leader,
  _grant_lvs_leadership) carries main's port-block RPCs, the removal
  relaxation of the unfenced bound, the set-leader budget and the
  two-node unblock guard; an abort releases every blocked peer
- the sync ANA failover reports whether a flip was attempted, so a
  skipped failover is retried by the monitor; a removed node attempts
  nothing and only an attempt is logged
- an active primary shut down by a removal counts as down for the site
  rule and the deferred promotion of its secondary, like an OFFLINE one
- the failback of a returning remote primary's secondary is housekeeping
  that never aborts the rebuild, as the home primary's
- the site-activation hold of migrations is a deferral
  (defer_unless_operable)
- cluster activation keeps main's arbitration flag and fences the sync
  leader with main's port-block RPCs
- the v2 cluster update refuses two-node arbitration on a
  sync-replication cluster (400) before any change
- the consistency-group peer serving-check probes every instance of a
  sync LVS, both sites' triplets
- the sync leadership move remembers the arbitration flag for the
  fence's unblock guard
- the Kubernetes drain steps (#1296: prepare-removal, migrate-devices)
  wait for a running site activation: a retryable refusal (503) before
  any change, as the removal task defers
- docs: the new refusal and the group fail-back no-op with policy-path
  async replication

Tests: sync fixtures route port fences through the RPC mocks, the
two-node consumers are pinned off on a sync cluster, and every
adaptation has a regression test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

target/main Targets the development branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants