diff --git a/backend/src/api/nft_manager.py b/backend/src/api/nft_manager.py index 798c0de..f988cda 100644 --- a/backend/src/api/nft_manager.py +++ b/backend/src/api/nft_manager.py @@ -824,144 +824,4 @@ def exec_raw(req: RawCmdRequest): logger.exception("exec_raw failed") 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 position ' - - 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 chain_rules is None or len(chain_rules) == 0: - 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: - # handle may be int or convertible; do tolerant compare - try: - if r.get("handle") is not None and int(r.get("handle")) == int(handle): - source_rule = r - break - except Exception: - continue - 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") is not None and int(rr.get("handle")) == int(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=int(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 a 500 (partial success) - raise HTTPException(status_code=500, detail={"added": add_exec, "deleted": del_exec}) - - # success - return MoveResult(added=add_exec, deleted=del_exec) - - except HTTPException: - # re-raise so FastAPI handles it - raise - except Exception as e: - logger.exception("move_rule internal error") - raise HTTPException(status_code=500, detail=str(e)) - - app.include_router(router) \ No newline at end of file