add sort endpoint to nftmanager
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
This commit is contained in:
@@ -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)
|
# ruleset may be typed RulesetModel or raw textual string (fallback)
|
||||||
RulesetValue = Optional[Union[RulesetModel, str]]
|
RulesetValue = Optional[Union[RulesetModel, str]]
|
||||||
|
|
||||||
@@ -790,4 +818,138 @@ def exec_raw(req: RawCmdRequest):
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
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)
|
app.include_router(router)
|
||||||
Reference in New Issue
Block a user