add sort endpoint to nftmanager
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-02-28 14:35:22 +01:00
parent e70167cf91
commit 2c6cb2b7d9

View File

@@ -265,6 +265,34 @@ class CreateRuleRequest(BaseModel):
}
# ---------- Move endpoints: request/response models ----------
class MoveRequest(BaseModel):
"""
Request body for moving a rule. Exactly one of these should typically be provided:
- position: numeric index to insert at (0 = first)
- before_handle: insert before an existing handle in the same chain
- to_top: boolean
- to_bottom: boolean
family/table/chain are required to identify the chain.
"""
family: str = Field(..., example="bridge")
table: str = Field(..., example="filter")
chain: str = Field(..., example="forward")
position: Optional[int] = Field(None, description="Zero-based position to insert at (0 = top)")
before_handle: Optional[int] = Field(None, description="Insert before this handle (find its index and use that)")
to_top: Optional[bool] = Field(False, description="Move to top (equivalent to position=0)")
to_bottom: Optional[bool] = Field(False, description="Move to bottom (append)")
class MoveSubResult(BaseModel):
cmd: str
out: Optional[ExecResult] = None
err: Optional[str] = None
class MoveResult(BaseModel):
added: MoveSubResult
deleted: MoveSubResult
# ruleset may be typed RulesetModel or raw textual string (fallback)
RulesetValue = Optional[Union[RulesetModel, str]]
@@ -790,4 +818,138 @@ def exec_raw(req: RawCmdRequest):
raise HTTPException(status_code=500, detail=str(e))
# ---------- New endpoints: move rules ----------
@router.post("/rules/{handle}/move", response_model=MoveResult, summary="Move rule to a new position within the same chain")
def move_rule(handle: int, req: MoveRequest):
"""
Move a rule identified by its handle to a new position in the same chain.
Strategy:
- read JSON ruleset to find the rule with given handle and its expr
- render expr -> textual fragment with expr_to_text
- add rule at desired position with 'add rule <family> <table> <chain> position <n> <expr>'
- delete the original rule by handle
Notes:
- If expr_to_text cannot deterministically render the expr, the endpoint returns 400.
- position is zero-based; position equal to number of rules appends.
"""
try:
# validate chain identity
family = req.family
table = req.table
chain = req.chain
# load ruleset JSON
try:
nft_json = mgr.list_rules_json()
except NftError as e:
logger.warning("move_rule: cannot read nft JSON: %s", e)
raise HTTPException(status_code=500, detail="Could not read nft JSON ruleset")
custom = build_predictable_ruleset(nft_json)
# find chain rules
chain_rules: List[Dict[str, Any]] = []
for t in custom.get("tables", []):
if t.get("family") == family and t.get("name") == table:
for ch in t.get("chains", []):
if ch.get("name") == chain:
chain_rules = ch.get("rules", [])
break
if not chain_rules:
raise HTTPException(status_code=404, detail="Chain not found or chain contains no rules")
# find the rule by handle
source_rule = None
for r in chain_rules:
if r.get("handle") == handle:
source_rule = r
break
if source_rule is None:
raise HTTPException(status_code=404, detail=f"Rule with handle {handle} not found in chain")
# render textual fragment
expr = source_rule.get("expr")
rendered = expr_to_text(expr)
if rendered is None:
raise HTTPException(status_code=400, detail="Cannot deterministically render rule expr to textual nft; move not possible via this endpoint")
# compute target position
target_pos: Optional[int] = None
if req.to_top:
target_pos = 0
elif req.to_bottom:
target_pos = len(chain_rules) # append
elif req.before_handle is not None:
# find index of before_handle
idx = next((i for i, rr in enumerate(chain_rules) if rr.get("handle") == req.before_handle), None)
if idx is None:
raise HTTPException(status_code=404, detail=f"before_handle {req.before_handle} not found in chain")
target_pos = idx
elif req.position is not None:
# clamp to [0, len]
target_pos = max(0, min(len(chain_rules), int(req.position)))
else:
raise HTTPException(status_code=400, detail="No target position specified (provide position, before_handle, to_top or to_bottom)")
# If the position corresponds to the same location as original and moving a later->earlier or vice versa might shift indices,
# we proceed anyway: simplest reliable approach is add at target_pos then delete original handle.
add_cmd = f"add rule {family} {table} {chain} position {target_pos} {rendered}"
logger.info("move_rule: add_cmd=%s (moving handle %s to pos %s)", add_cmd, handle, target_pos)
# Execute add
add_res_raw = mgr.cmd(add_cmd)
add_rc = int(add_res_raw.get("rc") or -1)
add_stdout = add_res_raw.get("stdout") or ""
add_stderr = add_res_raw.get("stderr") or ""
add_exec = MoveSubResult(
cmd=add_cmd,
out=ExecResult(rc=add_rc, stdout=add_stdout or None, stderr=add_stderr or None)
if add_res_raw is not None
else None,
err=None,
)
# If add failed decisively (non-zero rc and stderr present) -> return error without deleting original
if add_rc != 0 and add_stderr.strip() != "":
add_exec.err = f"add failed: rc={add_rc}, stderr={add_stderr}"
logger.warning("move_rule: add failed: %s", add_exec.err)
raise HTTPException(status_code=400, detail=add_exec.err)
# If add produced rc !=0 but stderr empty, attempt to detect presence like you do elsewhere
if add_rc != 0 and add_stderr.strip() == "":
try:
chain_text = mgr.list_chain_text(family, table, chain) or ""
if rendered not in chain_text:
add_exec.err = f"add reported rc={add_rc} and rule not found in chain text"
logger.warning("move_rule: add ambiguous: %s", add_exec.err)
raise HTTPException(status_code=400, detail=add_exec.err)
# otherwise treat as success
add_exec.out = ExecResult(rc=0, stdout=add_stdout or None, stderr=add_stderr or None)
except Exception:
add_exec.err = f"add reported rc={add_rc}, and verification failed"
raise HTTPException(status_code=400, detail=add_exec.err)
# Now delete the original rule by handle
try:
mgr.delete_rule_by_handle_text(family=family, table=table, chain=chain, handle=handle)
del_exec = MoveSubResult(cmd=f"delete rule {family} {table} {chain} handle {handle}", out=ExecResult(rc=0, stdout="", stderr=""), err=None)
except Exception as e:
# We succeeded adding but failed deleting - report both
err_msg = f"added new rule but deleting original handle {handle} failed: {e}"
logger.exception(err_msg)
del_exec = MoveSubResult(cmd=f"delete rule {family} {table} {chain} handle {handle}", out=None, err=str(e))
# Return 500 to indicate partial failure
return MoveResult(added=add_exec, deleted=del_exec)
# success
return MoveResult(added=add_exec, deleted=del_exec)
except HTTPException:
raise
except Exception as e:
logger.exception("move_rule internal error")
raise HTTPException(status_code=500, detail=str(e))
app.include_router(router)