|
| 1 | +/* |
| 2 | + * Copyright (c) 2004-present, Facebook, Inc. |
| 3 | + * All rights reserved. |
| 4 | + * |
| 5 | + * This source code is licensed under the BSD-style license found in the |
| 6 | + * LICENSE file in the root directory of this source tree. An additional grant |
| 7 | + * of patent rights can be found in the PATENTS file in the same directory. |
| 8 | + * |
| 9 | + */ |
| 10 | + |
| 11 | +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/match/CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch.h" |
| 12 | + |
| 13 | +#include "fboss/cli/fboss2/CmdHandler.cpp" |
| 14 | + |
| 15 | +#include <fmt/core.h> |
| 16 | +#include <neteng/fboss/bgp/public_tld/configerator/structs/neteng/fboss/bgp/gen-cpp2/bgp_config_types.h> |
| 17 | +#include <functional> |
| 18 | +#include <iostream> |
| 19 | +#include <map> |
| 20 | +#include <optional> |
| 21 | +#include <ostream> |
| 22 | +#include <stdexcept> |
| 23 | +#include <string> |
| 24 | +#include <string_view> |
| 25 | +#include <vector> |
| 26 | +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/bgp_policy_types.h" |
| 27 | +#include "configerator/structs/neteng/bgp_policy/thrift/gen-cpp2/routing_policy_types.h" |
| 28 | +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliAttrHandlers.h" |
| 29 | +#include "fboss/cli/fboss2/commands/config/protocol/bgp/BgpCliValueParsers.h" |
| 30 | +#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/BgpRoutingPolicyCliUtils.h" |
| 31 | +#include "fboss/cli/fboss2/session/ConfigSession.h" |
| 32 | +#include "fboss/cli/fboss2/utils/CmdUtilsCommon.h" |
| 33 | +#include "fboss/cli/fboss2/utils/HostInfo.h" |
| 34 | +#include "fmt/format.h" |
| 35 | + |
| 36 | +namespace facebook::fboss { |
| 37 | + |
| 38 | +namespace { |
| 39 | + |
| 40 | +// The composed `from <attribute>` dispatch keys, exactly as documented. Kept |
| 41 | +// here so the valid-attribute set and the handler table stay in sync. |
| 42 | +// |
| 43 | +// Four of the seven documented `match from` attributes are NOT offered, |
| 44 | +// because bgpd cannot apply them (each was proven by crash-looping bgpd on a |
| 45 | +// device, then confirmed in private-BGP): |
| 46 | +// |
| 47 | +// - local-pref, med, next-hop (NOS-6683/6684/6685): |
| 48 | +// createPolicyAttributeMatchItem has no LOCAL_PREFERENCE, MED or NEXT_HOP |
| 49 | +// case, so those atomic types hit `default:` and throw |
| 50 | +// "BgpPolicyAtomicMatch Config input error for type". |
| 51 | +// - community-list (NOS-6682): CommunityMatch's constructor throws |
| 52 | +// "The attribute \"communities\" is empty" unless the inline communities |
| 53 | +// list is non-empty, and CommunityMatch::PopulateReferences — the only |
| 54 | +// thing that resolves community_list_names — runs later, from |
| 55 | +// PolicyManager, once every Policy is already constructed. The by-name |
| 56 | +// reference path is therefore unreachable; only inlined community values |
| 57 | +// work, which is a snapshot rather than a reference. |
| 58 | +// |
| 59 | +// All four need a private-BGP change before the CLI can accept them. |
| 60 | +constexpr std::string_view kFromKeyword = "from"; |
| 61 | +constexpr std::string_view kFromAsPathList = "from as-path-list"; |
| 62 | +constexpr std::string_view kFromOrigin = "from origin"; |
| 63 | +constexpr std::string_view kFromPrefixList = "from prefix-list"; |
| 64 | + |
| 65 | +// origin values (bgp_policy.Origin names). |
| 66 | +constexpr std::string_view kOriginIgp = "IGP"; |
| 67 | +constexpr std::string_view kOriginEgp = "EGP"; |
| 68 | +constexpr std::string_view kOriginIncomplete = "INCOMPLETE"; |
| 69 | + |
| 70 | +using BgpPolicyAtomicMatch = bgp::bgp_policy::BgpPolicyAtomicMatch; |
| 71 | +using BgpPolicyAtomicMatchType = bgp::bgp_policy::BgpPolicyAtomicMatchType; |
| 72 | +using BgpPolicyTerm = bgp::bgp_policy::BgpPolicyTerm; |
| 73 | +using Origin = bgp::bgp_policy::Origin; |
| 74 | +using bgpcli::AttrHandler; |
| 75 | +using bgpcli::enumAttr; |
| 76 | +using bgpcli::Result; |
| 77 | +using bgpcli::stringAttr; |
| 78 | + |
| 79 | +// Find the atomic match of `type`, creating it (and the term's match object) |
| 80 | +// if absent. Atomic entries are keyed by BgpPolicyAtomicMatchType, so |
| 81 | +// re-issuing a match kind updates its entry rather than appending. |
| 82 | +// |
| 83 | +// The container is BgpPolicyTerm.policy_match_entries — a single |
| 84 | +// BgpPolicyMatch, marked @thrift.Deprecated but the ONLY one bgpd reads: |
| 85 | +// PolicyTerm.cpp guards its match loop on `if (term.policy_match_entries())`, |
| 86 | +// and nothing in private-BGP reads the newer `policy_matches` list at all, so |
| 87 | +// matches written there are silently ignored. Entries compose under |
| 88 | +// BgpPolicyMatch's default AND, which is also the only operator bgpd accepts |
| 89 | +// for more than one entry. |
| 90 | +BgpPolicyAtomicMatch& findOrCreateAtomicMatch( |
| 91 | + BgpPolicyTerm& term, |
| 92 | + BgpPolicyAtomicMatchType type) { |
| 93 | + auto& entries = *term.policy_match_entries().ensure().match_entries(); |
| 94 | + for (auto& entry : entries) { |
| 95 | + if (*entry.type() == type) { |
| 96 | + return entry; |
| 97 | + } |
| 98 | + } |
| 99 | + entries.emplace_back(); |
| 100 | + auto& entry = entries.back(); |
| 101 | + entry.type() = type; |
| 102 | + return entry; |
| 103 | +} |
| 104 | + |
| 105 | +// ---- match setters --------------------------------------------------------- |
| 106 | +// Each writes one already-parsed, already-validated value into the term's |
| 107 | +// atomic match of the matching type. Parsing and message text belong to the |
| 108 | +// shared factories in BgpCliAttrHandlers.h. |
| 109 | +// |
| 110 | +// The list-typed matches take a full inline object rather than a name-only |
| 111 | +// union arm, and a reference is expressed by its *_list_names field — NOT by |
| 112 | +// the object's `name`, which is only a label. bgpd's match class keeps those |
| 113 | +// names, then PolicyManager::PopulateReferences resolves each against its |
| 114 | +// by-name map and merges in the referenced list's contents. Writing `name` |
| 115 | +// instead leaves the match with nothing to compare against, so it silently |
| 116 | +// matches nothing rather than failing loudly. |
| 117 | + |
| 118 | +void setAsPathList(BgpPolicyTerm& term, const std::string& name) { |
| 119 | + findOrCreateAtomicMatch(term, BgpPolicyAtomicMatchType::AS_PATH) |
| 120 | + .as_path_filters() |
| 121 | + .ensure() |
| 122 | + .as_path_list_names() |
| 123 | + .ensure() = {name}; |
| 124 | +} |
| 125 | + |
| 126 | +void setOrigin(BgpPolicyTerm& term, Origin origin) { |
| 127 | + findOrCreateAtomicMatch(term, BgpPolicyAtomicMatchType::ORIGIN).origin() = |
| 128 | + origin; |
| 129 | +} |
| 130 | + |
| 131 | +void setPrefixList(BgpPolicyTerm& term, const std::string& name) { |
| 132 | + findOrCreateAtomicMatch(term, BgpPolicyAtomicMatchType::PREFIX_LIST) |
| 133 | + .prefix_filters() |
| 134 | + .ensure() |
| 135 | + .prefix_list_names() = {name}; |
| 136 | +} |
| 137 | + |
| 138 | +std::optional<Origin> parseOrigin(const std::string& value) { |
| 139 | + if (value == kOriginIgp) { |
| 140 | + return Origin::IGP; |
| 141 | + } |
| 142 | + if (value == kOriginEgp) { |
| 143 | + return Origin::EGP; |
| 144 | + } |
| 145 | + if (value == kOriginIncomplete) { |
| 146 | + return Origin::INCOMPLETE; |
| 147 | + } |
| 148 | + return std::nullopt; |
| 149 | +} |
| 150 | + |
| 151 | +// ---- match attribute registry ---------------------------------------------- |
| 152 | +// One line per supported attribute: its dispatch key, its value shape, and |
| 153 | +// the setter that stores it. |
| 154 | +const std::map<std::string, AttrHandler<BgpPolicyTerm>, std::less<>>& |
| 155 | +matchAttrHandlers() { |
| 156 | + static const std::string kOriginValues = |
| 157 | + fmt::format("{}|{}|{}", kOriginIgp, kOriginEgp, kOriginIncomplete); |
| 158 | + static const std::map<std::string, AttrHandler<BgpPolicyTerm>, std::less<>> |
| 159 | + kHandlers = { |
| 160 | + {std::string(kFromAsPathList), |
| 161 | + stringAttr<BgpPolicyTerm>("as-path-list", "name", setAsPathList)}, |
| 162 | + {std::string(kFromOrigin), |
| 163 | + enumAttr<BgpPolicyTerm, Origin>( |
| 164 | + "origin", kOriginValues, parseOrigin, setOrigin)}, |
| 165 | + {std::string(kFromPrefixList), |
| 166 | + stringAttr<BgpPolicyTerm>("prefix-list", "name", setPrefixList)}, |
| 167 | + }; |
| 168 | + return kHandlers; |
| 169 | +} |
| 170 | + |
| 171 | +std::string validAttrList() { |
| 172 | + std::string out; |
| 173 | + for (const auto& [name, _] : matchAttrHandlers()) { |
| 174 | + if (!out.empty()) { |
| 175 | + out += ", "; |
| 176 | + } |
| 177 | + out += name; |
| 178 | + } |
| 179 | + return out; |
| 180 | +} |
| 181 | + |
| 182 | +} // namespace |
| 183 | + |
| 184 | +// Parse + validate at construction so queryClient stays a thin dispatch. |
| 185 | +// Throwing std::invalid_argument is how the framework surfaces arg parse |
| 186 | +// errors (same mechanism as BgpRoutingPolicyTermActionSetConfig). |
| 187 | +BgpRoutingPolicyTermMatchConfig::BgpRoutingPolicyTermMatchConfig( |
| 188 | + std::vector<std::string> v) |
| 189 | + : utils::BaseObjectArgType<std::string>(v) { |
| 190 | + if (v.empty() || v[0] != kFromKeyword) { |
| 191 | + throw std::invalid_argument( |
| 192 | + "Error: match requires `from <attribute> <value>`"); |
| 193 | + } |
| 194 | + if (v.size() < 2) { |
| 195 | + throw std::invalid_argument( |
| 196 | + fmt::format( |
| 197 | + "Error: `from` requires an <attribute>. Valid attributes: {}", |
| 198 | + validAttrList())); |
| 199 | + } |
| 200 | + attr_ = fmt::format("{} {}", kFromKeyword, v[1]); |
| 201 | + values_.assign(v.begin() + 2, v.end()); |
| 202 | + |
| 203 | + if (matchAttrHandlers().find(attr_) == matchAttrHandlers().end()) { |
| 204 | + throw std::invalid_argument( |
| 205 | + fmt::format( |
| 206 | + "Error: unknown match attribute '{}'. Valid attributes: {}", |
| 207 | + attr_, |
| 208 | + validAttrList())); |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +CmdConfigProtocolBgpPolicyRoutingPolicyTermMatchTraits::RetType |
| 213 | +CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch::queryClient( |
| 214 | + const HostInfo& /* hostInfo */, |
| 215 | + const BgpRoutingPolicyConfig& policyArgs, |
| 216 | + const BgpRoutingPolicyTermConfig& termArgs, |
| 217 | + const ObjectArgType& args) { |
| 218 | + // Ancestor-level attributes mixed with a match command parse, but only |
| 219 | + // the leaf (this command) runs — reject instead of silently dropping them. |
| 220 | + if (!policyArgs.attr().empty() || !termArgs.attr().empty()) { |
| 221 | + return fmt::format( |
| 222 | + "Error: configure routing-policy/term attributes and match in " |
| 223 | + "separate commands (got attribute '{}' alongside match)", |
| 224 | + !policyArgs.attr().empty() ? policyArgs.attr() : termArgs.attr()); |
| 225 | + } |
| 226 | + |
| 227 | + auto& session = ConfigSession::getInstance(); |
| 228 | + auto& cfg = session.getBgpConfig(); |
| 229 | + const bool policyCreated = |
| 230 | + !bgpcli::routingPolicyExists(cfg, policyArgs.policyName()); |
| 231 | + auto& policy = |
| 232 | + bgpcli::findOrCreateRoutingPolicy(cfg, policyArgs.policyName()); |
| 233 | + const bool termCreated = |
| 234 | + !bgpcli::routingPolicyTermExists(policy, termArgs.seqNum()); |
| 235 | + auto& term = bgpcli::findOrCreateRoutingPolicyTerm(policy, termArgs.seqNum()); |
| 236 | + |
| 237 | + // The attribute is guaranteed valid: BgpRoutingPolicyTermMatchConfig's |
| 238 | + // constructor rejects an unknown attribute before we get here. |
| 239 | + Result result = |
| 240 | + matchAttrHandlers().find(args.attr())->second(term, args.values()); |
| 241 | + |
| 242 | + if (result.ok) { |
| 243 | + result.message += fmt::format( |
| 244 | + " for routing-policy {} term {} match", |
| 245 | + policyArgs.policyName(), |
| 246 | + termArgs.seqNum()); |
| 247 | + session.saveBgpConfig(); |
| 248 | + result.message += |
| 249 | + fmt::format("\nConfig saved to: {}", session.getBgpSessionConfigPath()); |
| 250 | + } else { |
| 251 | + // A rejected value must not leave a phantom term (or a phantom policy |
| 252 | + // implicitly created for it) visible to later lookups in this process. |
| 253 | + if (termCreated) { |
| 254 | + policy.policy_entries()->pop_back(); |
| 255 | + } |
| 256 | + if (policyCreated) { |
| 257 | + cfg.policies()->bgp_policy_statements()->pop_back(); |
| 258 | + } |
| 259 | + } |
| 260 | + return result.message; |
| 261 | +} |
| 262 | + |
| 263 | +void CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch::printOutput( |
| 264 | + const RetType& output) { |
| 265 | + std::cout << output << std::endl; |
| 266 | +} |
| 267 | + |
| 268 | +// Explicit template instantiation |
| 269 | +template void CmdHandler< |
| 270 | + CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch, |
| 271 | + CmdConfigProtocolBgpPolicyRoutingPolicyTermMatchTraits>::run(); |
| 272 | + |
| 273 | +} // namespace facebook::fboss |
0 commit comments