From 84a46adaa65fddce444d249d7d55005af048d126 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 12:23:22 +0000 Subject: [PATCH] =?UTF-8?q?0.9.24=20=E2=80=94=20rootless:=20bridge=5Fadd?= =?UTF-8?q?=5Fmember=20+=20bridge=5Fdel=5Fmember=20verbs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twenty-fifth 0.9.x release. Symmetric pair around IfconfigOps::bridgeAddMember / bridgeDelMember. Two new privops verbs (2-arg shape: bridge + member, both validated as iface names): bridge_add_member — wraps IfconfigOps::bridgeAddMember bridge_del_member — symmetric remove Same wire-up pattern as 0.9.23: privops_pure (enum + structs + validators), privops_wire_pure (JSON parse + format), privops_ nv_pure (nv parse), privops_client (build), privops_handlers (handle + dispatcher cases for both transports). CLI wiring in lib/run_net.cpp: 2 new privops-aware helpers, 2 call-site replacements (setupBridgeEpair add, destroyBridge Epair del). Add hard-fails; del soft-fails (RunAtEnd pattern). 2 new ATF tests + verb_token_roundtrips_for_every_verb update. Suite: 1296 -> 1298. Remaining iface verbs: set_iface_inet_addr (0.9.25), create_epair (0.9.26 — first response-data verb). --- CHANGELOG.md | 78 ++++++++++++++++++++++++++++++++ cli/args.cpp | 4 +- daemon/privops_handlers.cpp | 54 ++++++++++++++++++++++ daemon/privops_handlers.h | 5 ++ lib/privops_client.h | 6 +++ lib/privops_client_pure.cpp | 18 ++++++++ lib/privops_nv_pure.cpp | 14 ++++++ lib/privops_nv_pure.h | 4 ++ lib/privops_pure.cpp | 16 +++++++ lib/privops_pure.h | 25 ++++++++++ lib/privops_wire_pure.cpp | 38 ++++++++++++++++ lib/privops_wire_pure.h | 12 +++++ lib/run_net.cpp | 44 +++++++++++++++++- tests/unit/privops_pure_test.cpp | 32 +++++++++++++ 14 files changed, 346 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda2d51..5fb52bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,84 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [0.9.24] — 2026-05-09 + +**Rootless track, bridge membership verbs.** Twenty-fifth +0.9.x release. Symmetric pair around `IfconfigOps::bridgeAddMember` +and `bridgeDelMember`. + +### What lands + +#### Two new privops verbs + +- **`bridge_add_member`** — wraps `IfconfigOps::bridgeAddMember(bridge, member)`. + Fields: `bridge`, `member` (both validated as iface names). +- **`bridge_del_member`** — symmetric remove. + +Different shape from 0.9.23's verbs (2 args vs 1) but same +overall pattern. Validators delegate to existing +`validateIfaceName` for both fields. + +#### Wire-up across the stack + +Same files as 0.9.23 — `privops_pure`, `privops_wire_pure`, +`privops_nv_pure`, `privops_client`, `privops_handlers`. Each +gained two new functions / cases. + +#### CLI wiring + +`lib/run_net.cpp` gets two more file-static helpers +(`bridgeAddMemberPrivopsOrLocal`, `bridgeDelMemberPrivopsOrLocal`) +and 2 call-sites migrate: + +| Site | Op | +|------|-----| +| `setupBridgeEpair` line 481 | `bridgeAddMember(bridgeIface, ifaceA)` | +| `destroyBridgeEpair` line 491 | `bridgeDelMember(bridgeIface, ifaceA)` | + +`bridgeAddMember` is hard-fail (matches existing exception +behaviour). `bridgeDelMember` is soft-fail (matches RunAtEnd +teardown pattern from earlier releases — warn on error, +continue cleanup). + +### Series state + +CLI call-sites wired: +- `crate retune` (0.9.15) +- `crate stop` (0.9.17) +- `crate run` ZFS attach + detach (0.9.18) +- `crate run` nullfs mounts 8 sites (0.9.19) +- `crate run` vnet moveToVnet 4 sites (0.9.20) +- `crate run` removeJail teardown (0.9.21) +- `crate run` createJail (0.9.22) +- `crate run` setUp + disableOffload 5 sites (0.9.23) +- **`crate run` bridge add + del 2 sites → bridge_add_member / bridge_del_member ← this release** + +Remaining iface verbs: +- 0.9.25 — `set_iface_inet_addr` (3-arg verb: iface + addr + prefix_len) +- 0.9.26 — `create_epair` (first response-data verb — returns the epair pair names) +- 0.9.27 — `network_lease.cpp` per-user paths + RCTL umbrella +- 0.9.28 — default flip +- 1.0.0 — setuid removed + +### Tests + +2 new ATF tests (`bridge_add_member_minimal`, +`bridge_del_member_minimal`) in `privops_pure_test`. +`verb_token_roundtrips_for_every_verb` updated to include +both new verbs. Suite: 1296 → **1298**. + +### Files + +Same set as 0.9.23: `privops_pure.{h,cpp}`, +`privops_wire_pure.{h,cpp}`, `privops_nv_pure.{h,cpp}`, +`privops_client.h`, `privops_client_pure.cpp`, +`privops_handlers.{h,cpp}`, `run_net.cpp`, +`tests/unit/privops_pure_test.cpp`, `cli/args.cpp`, +`CHANGELOG.md`. + +--- + ## [0.9.23] — 2026-05-09 **Rootless track, atomic single-iface verbs.** Twenty-fourth diff --git a/cli/args.cpp b/cli/args.cpp index b1fc2f6..e113e87 100644 --- a/cli/args.cpp +++ b/cli/args.cpp @@ -753,7 +753,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.noColor = true; break; } else if (strEq(argv[a], "--version")) { - std::cout << "crate 0.9.23" << std::endl; + std::cout << "crate 0.9.24" << std::endl; exit(0); } else if (auto argShort = isShort(argv[a])) { switch (argShort) { @@ -764,7 +764,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.logProgress = true; break; case 'V': - std::cout << "crate 0.9.23" << std::endl; + std::cout << "crate 0.9.24" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/daemon/privops_handlers.cpp b/daemon/privops_handlers.cpp index e81b326..82ed291 100644 --- a/daemon/privops_handlers.cpp +++ b/daemon/privops_handlers.cpp @@ -394,6 +394,28 @@ DispatchResult handleDisableIfaceOffload(const PrivOpsPure::DisableIfaceOffloadR return {200, PrivOpsWirePure::formatDisableIfaceOffloadSuccess(r.ifname)}; } +// --- handleBridgeAddMember / handleBridgeDelMember (0.9.24) --- + +DispatchResult handleBridgeAddMember(const PrivOpsPure::BridgeAddMemberReq &r) { + try { + IfconfigOps::bridgeAddMember(r.bridge, r.member); + } catch (const std::exception &e) { + return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed", + e.what())}; + } + return {200, PrivOpsWirePure::formatBridgeAddMemberSuccess(r.bridge, r.member)}; +} + +DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeDelMemberReq &r) { + try { + IfconfigOps::bridgeDelMember(r.bridge, r.member); + } catch (const std::exception &e) { + return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed", + e.what())}; + } + return {200, PrivOpsWirePure::formatBridgeDelMemberSuccess(r.bridge, r.member)}; +} + // --- Top-level dispatcher --- namespace { @@ -546,6 +568,22 @@ DispatchResult dispatchPrivOp(Verb v, const std::string &body, return {400, PrivOpsWirePure::formatValidateError(e)}; return handleDisableIfaceOffload(r); } + case Verb::BridgeAddMember: { + PrivOpsPure::BridgeAddMemberReq r; + if (auto e = PrivOpsWirePure::parseBridgeAddMember(body, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateBridgeAddMember(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleBridgeAddMember(r); + } + case Verb::BridgeDelMember: { + PrivOpsPure::BridgeDelMemberReq r; + if (auto e = PrivOpsWirePure::parseBridgeDelMember(body, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateBridgeDelMember(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleBridgeDelMember(r); + } default: return PrivOpsWirePure::parseValidateAndDispatch(v, body); } @@ -692,6 +730,22 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m, return {400, PrivOpsWirePure::formatValidateError(e)}; return handleDisableIfaceOffload(r); } + case Verb::BridgeAddMember: { + PrivOpsPure::BridgeAddMemberReq r; + if (auto e = PrivOpsNvPure::parseBridgeAddMember(m, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateBridgeAddMember(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleBridgeAddMember(r); + } + case Verb::BridgeDelMember: { + PrivOpsPure::BridgeDelMemberReq r; + if (auto e = PrivOpsNvPure::parseBridgeDelMember(m, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateBridgeDelMember(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleBridgeDelMember(r); + } case Verb::Unknown: return {404, std::string("{\"error\":\"unknown or missing 'verb' field\"}")}; diff --git a/daemon/privops_handlers.h b/daemon/privops_handlers.h index 152acf4..99d8a31 100644 --- a/daemon/privops_handlers.h +++ b/daemon/privops_handlers.h @@ -152,4 +152,9 @@ PrivOpsWirePure::DispatchResult handleDestroyJail(const PrivOpsPure::DestroyJail PrivOpsWirePure::DispatchResult handleSetIfaceUp(const PrivOpsPure::SetIfaceUpReq &r); PrivOpsWirePure::DispatchResult handleDisableIfaceOffload(const PrivOpsPure::DisableIfaceOffloadReq &r); +// 0.9.24: bridge membership ops. Wraps +// IfconfigOps::bridgeAddMember / bridgeDelMember. +PrivOpsWirePure::DispatchResult handleBridgeAddMember(const PrivOpsPure::BridgeAddMemberReq &r); +PrivOpsWirePure::DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeDelMemberReq &r); + } // namespace Crated diff --git a/lib/privops_client.h b/lib/privops_client.h index de4e44a..26b64e7 100644 --- a/lib/privops_client.h +++ b/lib/privops_client.h @@ -112,6 +112,12 @@ PrivOpsNvPure::FieldMap buildDestroyJail(const std::string &name, PrivOpsNvPure::FieldMap buildSetIfaceUp(const std::string &ifname); PrivOpsNvPure::FieldMap buildDisableIfaceOffload(const std::string &ifname); +// 0.9.24: bridge membership ops. +PrivOpsNvPure::FieldMap buildBridgeAddMember(const std::string &bridge, + const std::string &member); +PrivOpsNvPure::FieldMap buildBridgeDelMember(const std::string &bridge, + const std::string &member); + // --- Wire transport (FreeBSD-only) --- struct Response { diff --git a/lib/privops_client_pure.cpp b/lib/privops_client_pure.cpp index 725887e..ea1353f 100644 --- a/lib/privops_client_pure.cpp +++ b/lib/privops_client_pure.cpp @@ -233,4 +233,22 @@ PrivOpsNvPure::FieldMap buildDisableIfaceOffload(const std::string &ifname) { }; } +PrivOpsNvPure::FieldMap buildBridgeAddMember(const std::string &bridge, + const std::string &member) { + return { + {"verb", "bridge_add_member"}, + {"bridge", bridge}, + {"member", member}, + }; +} + +PrivOpsNvPure::FieldMap buildBridgeDelMember(const std::string &bridge, + const std::string &member) { + return { + {"verb", "bridge_del_member"}, + {"bridge", bridge}, + {"member", member}, + }; +} + } // namespace PrivOpsClient diff --git a/lib/privops_nv_pure.cpp b/lib/privops_nv_pure.cpp index 97eaa96..f730587 100644 --- a/lib/privops_nv_pure.cpp +++ b/lib/privops_nv_pure.cpp @@ -212,6 +212,20 @@ std::string parseDisableIfaceOffload(const FieldMap &m, return ""; } +std::string parseBridgeAddMember(const FieldMap &m, + PrivOpsPure::BridgeAddMemberReq &out) { + if (auto e = requireString(m, "bridge", out.bridge); !e.empty()) return e; + if (auto e = requireString(m, "member", out.member); !e.empty()) return e; + return ""; +} + +std::string parseBridgeDelMember(const FieldMap &m, + PrivOpsPure::BridgeDelMemberReq &out) { + if (auto e = requireString(m, "bridge", out.bridge); !e.empty()) return e; + if (auto e = requireString(m, "member", out.member); !e.empty()) return e; + return ""; +} + // --- Verb routing --- PrivOpsPure::Verb extractVerb(const FieldMap &m) { diff --git a/lib/privops_nv_pure.h b/lib/privops_nv_pure.h index c9f4b05..c1c312c 100644 --- a/lib/privops_nv_pure.h +++ b/lib/privops_nv_pure.h @@ -123,6 +123,10 @@ std::string parseSetIfaceUp(const FieldMap &m, PrivOpsPure::SetIfaceUpReq &out); std::string parseDisableIfaceOffload(const FieldMap &m, PrivOpsPure::DisableIfaceOffloadReq &out); +std::string parseBridgeAddMember(const FieldMap &m, + PrivOpsPure::BridgeAddMemberReq &out); +std::string parseBridgeDelMember(const FieldMap &m, + PrivOpsPure::BridgeDelMemberReq &out); // --- Verb routing --- diff --git a/lib/privops_pure.cpp b/lib/privops_pure.cpp index 243f26b..2579b8f 100644 --- a/lib/privops_pure.cpp +++ b/lib/privops_pure.cpp @@ -87,6 +87,8 @@ const char *verbName(Verb v) { case Verb::RemoveIpfwRule: return "remove_ipfw_rule"; case Verb::SetIfaceUp: return "set_iface_up"; case Verb::DisableIfaceOffload: return "disable_iface_offload"; + case Verb::BridgeAddMember: return "bridge_add_member"; + case Verb::BridgeDelMember: return "bridge_del_member"; case Verb::Unknown: return "unknown"; } return "unknown"; @@ -109,6 +111,8 @@ Verb parseVerb(const std::string &name) { if (name == "remove_ipfw_rule") return Verb::RemoveIpfwRule; if (name == "set_iface_up") return Verb::SetIfaceUp; if (name == "disable_iface_offload") return Verb::DisableIfaceOffload; + if (name == "bridge_add_member") return Verb::BridgeAddMember; + if (name == "bridge_del_member") return Verb::BridgeDelMember; return Verb::Unknown; } @@ -477,4 +481,16 @@ std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r) { return validateIfaceName(r.ifname); } +std::string validateBridgeAddMember(const BridgeAddMemberReq &r) { + if (auto e = validateIfaceName(r.bridge); !e.empty()) return "bridge: " + e; + if (auto e = validateIfaceName(r.member); !e.empty()) return "member: " + e; + return ""; +} + +std::string validateBridgeDelMember(const BridgeDelMemberReq &r) { + if (auto e = validateIfaceName(r.bridge); !e.empty()) return "bridge: " + e; + if (auto e = validateIfaceName(r.member); !e.empty()) return "member: " + e; + return ""; +} + } // namespace PrivOpsPure diff --git a/lib/privops_pure.h b/lib/privops_pure.h index 130e212..680e605 100644 --- a/lib/privops_pure.h +++ b/lib/privops_pure.h @@ -107,6 +107,17 @@ enum class Verb { // and `IfconfigOps::disableOffload` call sites in lib/run_net.cpp. SetIfaceUp, DisableIfaceOffload, + + // 0.9.24: bridge membership ops. Symmetric pair around + // IfconfigOps::bridgeAddMember / bridgeDelMember. The 0.9.6 + // composite ConfigureIface verb embeds bridgeAddMember when + // its `bridge` field is non-empty, but only for the + // computed-pair-A pattern (epair host-side). These atomic + // verbs target run_net.cpp's setupBridgeEpair (add) and + // destroyBridgeEpair (del) where the iface to attach is + // operator-supplied directly. + BridgeAddMember, + BridgeDelMember, }; // Returns the verb's canonical wire-format token (lowercase, no @@ -211,6 +222,18 @@ struct DisableIfaceOffloadReq { std::string ifname; }; +// 0.9.24: bridge membership ops. Both validate via existing +// validateIfaceName for both `bridge` and `member` fields. +struct BridgeAddMemberReq { + std::string bridge; // bridge interface name (e.g. "bridge0") + std::string member; // member interface name (e.g. "epair0a") +}; + +struct BridgeDelMemberReq { + std::string bridge; + std::string member; +}; + // --- Per-verb validators --- // // Each `validate*(req)` returns "" on success, otherwise a one-line @@ -235,6 +258,8 @@ std::string validateAddIpfwRule(const AddIpfwRuleReq &r); std::string validateRemoveIpfwRule(const RemoveIpfwRuleReq &r); std::string validateSetIfaceUp(const SetIfaceUpReq &r); std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r); +std::string validateBridgeAddMember(const BridgeAddMemberReq &r); +std::string validateBridgeDelMember(const BridgeDelMemberReq &r); // --- Field-level validators (exposed for tests + reuse) --- // diff --git a/lib/privops_wire_pure.cpp b/lib/privops_wire_pure.cpp index 4da1f66..7561abe 100644 --- a/lib/privops_wire_pure.cpp +++ b/lib/privops_wire_pure.cpp @@ -348,6 +348,20 @@ std::string parseDisableIfaceOffload(const std::string &body, return ""; } +std::string parseBridgeAddMember(const std::string &body, + PrivOpsPure::BridgeAddMemberReq &out) { + if (auto e = requireStringField(body, "bridge", out.bridge); !e.empty()) return e; + if (auto e = requireStringField(body, "member", out.member); !e.empty()) return e; + return ""; +} + +std::string parseBridgeDelMember(const std::string &body, + PrivOpsPure::BridgeDelMemberReq &out) { + if (auto e = requireStringField(body, "bridge", out.bridge); !e.empty()) return e; + if (auto e = requireStringField(body, "member", out.member); !e.empty()) return e; + return ""; +} + // --- Verb routing helper --- PrivOpsPure::Verb parseVerbFromPath(const std::string &path) { @@ -602,6 +616,26 @@ std::string formatDisableIfaceOffloadSuccess(const std::string &ifname) { return o.str(); } +std::string formatBridgeAddMemberSuccess(const std::string &bridge, + const std::string &member) { + std::ostringstream o; + o << "{\"added\":true" + << ",\"bridge\":\"" << escape(bridge) << "\"" + << ",\"member\":\"" << escape(member) << "\"" + << "}"; + return o.str(); +} + +std::string formatBridgeDelMemberSuccess(const std::string &bridge, + const std::string &member) { + std::ostringstream o; + o << "{\"removed\":true" + << ",\"bridge\":\"" << escape(bridge) << "\"" + << ",\"member\":\"" << escape(member) << "\"" + << "}"; + return o.str(); +} + DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, const std::string &body) { using namespace PrivOpsPure; @@ -638,6 +672,10 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, return runVerb(body, v, parseSetIfaceUp, validateSetIfaceUp); case Verb::DisableIfaceOffload: return runVerb(body, v, parseDisableIfaceOffload, validateDisableIfaceOffload); + case Verb::BridgeAddMember: + return runVerb(body, v, parseBridgeAddMember, validateBridgeAddMember); + case Verb::BridgeDelMember: + return runVerb(body, v, parseBridgeDelMember, validateBridgeDelMember); case Verb::Unknown: break; } diff --git a/lib/privops_wire_pure.h b/lib/privops_wire_pure.h index d9fd4a6..57fb72e 100644 --- a/lib/privops_wire_pure.h +++ b/lib/privops_wire_pure.h @@ -162,6 +162,12 @@ std::string parseSetIfaceUp(const std::string &body, std::string parseDisableIfaceOffload(const std::string &body, PrivOpsPure::DisableIfaceOffloadReq &out); +std::string parseBridgeAddMember(const std::string &body, + PrivOpsPure::BridgeAddMemberReq &out); + +std::string parseBridgeDelMember(const std::string &body, + PrivOpsPure::BridgeDelMemberReq &out); + // --- Verb routing helper --- // // Parse the URL path's verb segment. The route pattern is @@ -271,4 +277,10 @@ std::string formatDestroyJailSuccess(const std::string &name); std::string formatSetIfaceUpSuccess(const std::string &ifname); std::string formatDisableIfaceOffloadSuccess(const std::string &ifname); +// 0.9.24: 200 OK bodies for bridge membership ops. +std::string formatBridgeAddMemberSuccess(const std::string &bridge, + const std::string &member); +std::string formatBridgeDelMemberSuccess(const std::string &bridge, + const std::string &member); + } // namespace PrivOpsWirePure diff --git a/lib/run_net.cpp b/lib/run_net.cpp index c3fca61..7889cb0 100644 --- a/lib/run_net.cpp +++ b/lib/run_net.cpp @@ -98,6 +98,46 @@ static void disableOffloadPrivopsOrLocal(const std::string &iface) { IfconfigOps::disableOffload(iface); } +// 0.9.24: privops-aware wrappers for bridge add/del. +static void bridgeAddMemberPrivopsOrLocal(const std::string &bridge, + const std::string &member) { + std::string sock = PrivOpsClient::detectSocketPath(); + if (!sock.empty()) { + auto resp = PrivOpsClient::sendRequest(sock, + PrivOpsClient::buildBridgeAddMember(bridge, member)); + if (!resp.transportError.empty()) + ERR2("run_net", "privops bridge_add_member '" << member << "' on '" + << bridge << "' transport error: " << resp.transportError) + if (resp.status >= 400) + ERR2("run_net", "privops bridge_add_member '" << member << "' on '" + << bridge << "' failed (status " << resp.status << "): " + << resp.body) + return; + } + IfconfigOps::bridgeAddMember(bridge, member); +} + +static void bridgeDelMemberPrivopsOrLocal(const std::string &bridge, + const std::string &member) { + std::string sock = PrivOpsClient::detectSocketPath(); + if (!sock.empty()) { + auto resp = PrivOpsClient::sendRequest(sock, + PrivOpsClient::buildBridgeDelMember(bridge, member)); + // Soft-fail on teardown — bridge may already be gone. + if (!resp.transportError.empty() || resp.status >= 400) { + std::cerr << rang::fg::yellow + << "run_net: privops bridge_del_member '" << member + << "' on '" << bridge << "' ignored: " + << (resp.transportError.empty() + ? std::to_string(resp.status) + ": " + resp.body + : resp.transportError) + << rang::style::reset << std::endl; + } + return; + } + IfconfigOps::bridgeDelMember(bridge, member); +} + GatewayInfo detectGateway() { GatewayInfo gw; @@ -438,7 +478,7 @@ BridgeInfo createBridgeEpair(int jid, const std::string &jidStr, // bring host-side up and add to bridge setUpPrivopsOrLocal(info.ifaceA); - IfconfigOps::bridgeAddMember(bridgeIface, info.ifaceA); + bridgeAddMemberPrivopsOrLocal(bridgeIface, info.ifaceA); // move jail-side into jail moveToVnetPrivopsOrLocal(info.ifaceB, jid); @@ -448,7 +488,7 @@ BridgeInfo createBridgeEpair(int jid, const std::string &jidStr, void destroyBridgeEpair(const BridgeInfo &info) { // remove from bridge first, then destroy - IfconfigOps::bridgeDelMember(info.bridgeIface, info.ifaceA); + bridgeDelMemberPrivopsOrLocal(info.bridgeIface, info.ifaceA); IfconfigOps::destroyInterface(info.ifaceA); } diff --git a/tests/unit/privops_pure_test.cpp b/tests/unit/privops_pure_test.cpp index 66e72e0..1070bbf 100644 --- a/tests/unit/privops_pure_test.cpp +++ b/tests/unit/privops_pure_test.cpp @@ -24,6 +24,7 @@ ATF_TEST_CASE_BODY(verb_token_roundtrips_for_every_verb) { Verb::AddPfRule, Verb::RemovePfRule, Verb::AddIpfwRule, Verb::RemoveIpfwRule, Verb::SetIfaceUp, Verb::DisableIfaceOffload, + Verb::BridgeAddMember, Verb::BridgeDelMember, }; for (Verb v : verbs) { std::string token = verbName(v); @@ -509,6 +510,35 @@ ATF_TEST_CASE_BODY(disable_iface_offload_minimal) { ATF_REQUIRE(!validateDisableIfaceOffload(r).empty()); } +ATF_TEST_CASE_WITHOUT_HEAD(bridge_add_member_minimal); +ATF_TEST_CASE_BODY(bridge_add_member_minimal) { + BridgeAddMemberReq r; + r.bridge = "bridge0"; + r.member = "epair0a"; + ATF_REQUIRE_EQ(validateBridgeAddMember(r), std::string()); + // Empty member -> error + r.member = ""; + std::string e = validateBridgeAddMember(r); + ATF_REQUIRE(!e.empty()); + ATF_REQUIRE(e.find("member") != std::string::npos); + // Empty bridge -> error + r.bridge = ""; + r.member = "epair0a"; + e = validateBridgeAddMember(r); + ATF_REQUIRE(!e.empty()); + ATF_REQUIRE(e.find("bridge") != std::string::npos); +} + +ATF_TEST_CASE_WITHOUT_HEAD(bridge_del_member_minimal); +ATF_TEST_CASE_BODY(bridge_del_member_minimal) { + BridgeDelMemberReq r; + r.bridge = "bridge0"; + r.member = "epair0a"; + ATF_REQUIRE_EQ(validateBridgeDelMember(r), std::string()); + r.bridge = "name with space"; + ATF_REQUIRE(!validateBridgeDelMember(r).empty()); +} + ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, verb_token_roundtrips_for_every_verb); ATF_ADD_TEST_CASE(tcs, verb_unknown_token_returns_unknown); @@ -568,4 +598,6 @@ ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, teardown_iface_minimal); ATF_ADD_TEST_CASE(tcs, set_iface_up_minimal); ATF_ADD_TEST_CASE(tcs, disable_iface_offload_minimal); + ATF_ADD_TEST_CASE(tcs, bridge_add_member_minimal); + ATF_ADD_TEST_CASE(tcs, bridge_del_member_minimal); }