Skip to content

Commit 9713d13

Browse files
NOS-6681, NOS-6686, NOS-6687: fboss2 bgp policy routing-policy term match
Adds `config protocol bgp policy routing-policy <name> term <seq-num> match from <attribute> <value>` — the match level of a routing-policy term, as a CLI11 subcommand with its own handler alongside `action`; the policy and term args arrive through the ancestor-args tuple, and ancestor attributes mixed with a match command are rejected since only the leaf runs. Scoped to the three match kinds bgpd actually implements. Reading private-BGP's PolicyTerm.cpp turned up two things the sheet's Thrift reference column does not convey, but its JSON field path column does: 1. bgpd reads BgpPolicyTerm.policy_match_entries — a single BgpPolicyMatch, marked @thrift.Deprecated. Its match loop is guarded by `if (term.policy_match_entries())`, and NOTHING in private-BGP reads the newer `policy_matches` list, so matches written there are silently ignored: the config round-trips through getRunningConfig while bgpd applies no match at all. Same for the atomic payloads — bgpd reads the deprecated as_path_filters/communities_filter/prefix_filters, not the newer as_path_list/community_list/prefix_list. 2. createPolicyAttributeMatchItem has no LOCAL_PREFERENCE, MED or NEXT_HOP case, so those atomic types hit `default:` and throw "BgpPolicyAtomicMatch Config input error for type", crash-looping the daemon. `from local-pref`, `from med` and `from next-hop` (NOS-6683/6684/6685) are therefore rejected at parse time rather than offered; they need a private-BGP change first, and a unit test pins that they stay rejected. 3. `from community-list` (NOS-6682) is rejected for a different reason: CommunityMatch's constructor throws "The attribute \"communities\" is empty" unless the inline communities list is non-empty, and CommunityMatch::PopulateReferences -- the only thing that resolves community_list_names -- runs later, from PolicyManager, once every Policy is already constructed. The by-name reference path is therefore unreachable, so only inlined values would work, which is a snapshot rather than a reference. Same unit test pins it as rejected. - `from <attr>` writes one BgpPolicyAtomicMatch per match kind (keyed by BgpPolicyAtomicMatchType; re-issuing a kind updates its entry) into policy_match_entries, composing under its default AND — which is also the only operator bgpd accepts for more than one entry: - as-path-list <name> (NOS-6681) -> AS_PATH, as_path_filters - origin <IGP|EGP|INCOMPLETE> (NOS-6686) -> ORIGIN, origin - prefix-list <name> (NOS-6687) -> PREFIX_LIST, prefix_filters The two list-typed matches take a full inline object with no name-only union arm, and bgpd treats that object's `name` as the reference: the match class keeps the name, then PolicyManager::PopulateReferences swaps in the real list from its by-name map. A name-only object is exactly how a by-name match is expressed. - setters are named functions, so the registry reads as a table: one line per attribute giving its dispatch key, value shape, and setter. - unit tests (8) + a commit-path integration test. The integration assertions are load-bearing here in a way staged-config ones are not: bgpd constructs a Policy from every statement, so a wrong encoding crash-loops it and fails the test rather than sitting inert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 946bdbf commit 9713d13

11 files changed

Lines changed: 810 additions & 23 deletions

cmake/CliFboss2.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,8 @@ add_library(fboss2_config_lib
904904
fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/CmdConfigProtocolBgpPolicyRoutingPolicyTerm.h
905905
fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/action/CmdConfigProtocolBgpPolicyRoutingPolicyTermAction.cpp
906906
fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/action/CmdConfigProtocolBgpPolicyRoutingPolicyTermAction.h
907+
fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/match/CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch.cpp
908+
fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/match/CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch.h
907909
fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.cpp
908910
fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h
909911
fboss/cli/fboss2/commands/config/ptp/transparent_clock/CmdConfigPtpTransparentClock.cpp

cmake/CliFboss2TestConfig.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ add_executable(fboss2_cmd_config_test
1717
fboss/cli/fboss2/test/config/CmdConfigBgpPolicyRoutingPolicyTest.cpp
1818
fboss/cli/fboss2/test/config/CmdConfigBgpPolicyRoutingPolicyTermTest.cpp
1919
fboss/cli/fboss2/test/config/CmdConfigBgpPolicyRoutingPolicyTermActionTest.cpp
20+
fboss/cli/fboss2/test/config/CmdConfigBgpPolicyRoutingPolicyTermMatchTest.cpp
2021
fboss/cli/fboss2/test/config/CmdConfigCoppTest.cpp
2122
fboss/cli/fboss2/test/config/CmdConfigDhcpTest.cpp
2223
fboss/cli/fboss2/test/config/CmdConfigHostnameTest.cpp

cmake/CliFboss2TestIntegrationTest.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_executable(fboss2_integration_test
1919
fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyRoutingPolicyTest.cpp
2020
fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyRoutingPolicyTermTest.cpp
2121
fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyRoutingPolicyTermActionTest.cpp
22+
fboss/cli/fboss2/test/integration_test/ConfigBgpPolicyRoutingPolicyTermMatchTest.cpp
2223
fboss/cli/fboss2/test/integration_test/ConfigBgpSessionTest.cpp
2324
fboss/cli/fboss2/test/integration_test/ConfigConcurrentSessionsTest.cpp
2425
fboss/cli/fboss2/test/integration_test/ConfigHostnameTest.cpp

fboss/cli/fboss2/BUCK

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,7 @@ cpp_library(
11101110
"commands/config/protocol/bgp/policy/routing-policy/CmdConfigProtocolBgpPolicyRoutingPolicy.cpp",
11111111
"commands/config/protocol/bgp/policy/routing-policy/term/CmdConfigProtocolBgpPolicyRoutingPolicyTerm.cpp",
11121112
"commands/config/protocol/bgp/policy/routing-policy/term/action/CmdConfigProtocolBgpPolicyRoutingPolicyTermAction.cpp",
1113+
"commands/config/protocol/bgp/policy/routing-policy/term/match/CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch.cpp",
11131114
"commands/config/protocol/static/CmdConfigProtocolStatic.cpp",
11141115
"commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.cpp",
11151116
"commands/config/ptp/CmdConfigPtp.cpp",
@@ -1237,6 +1238,7 @@ cpp_library(
12371238
"commands/config/protocol/bgp/policy/routing-policy/BgpRoutingPolicyCliUtils.h",
12381239
"commands/config/protocol/bgp/policy/routing-policy/term/CmdConfigProtocolBgpPolicyRoutingPolicyTerm.h",
12391240
"commands/config/protocol/bgp/policy/routing-policy/term/action/CmdConfigProtocolBgpPolicyRoutingPolicyTermAction.h",
1241+
"commands/config/protocol/bgp/policy/routing-policy/term/match/CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch.h",
12401242
"commands/config/protocol/static/CmdConfigProtocolStatic.h",
12411243
"commands/config/protocol/static/route/StaticRouteUtils.h",
12421244
"commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h",

fboss/cli/fboss2/CmdListConfig.cpp

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/CmdConfigProtocolBgpPolicyRoutingPolicy.h"
5151
#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/CmdConfigProtocolBgpPolicyRoutingPolicyTerm.h"
5252
#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/action/CmdConfigProtocolBgpPolicyRoutingPolicyTermAction.h"
53+
#include "fboss/cli/fboss2/commands/config/protocol/bgp/policy/routing-policy/term/match/CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch.h"
5354
#include "fboss/cli/fboss2/commands/config/protocol/static/CmdConfigProtocolStatic.h"
5455
#include "fboss/cli/fboss2/commands/config/protocol/static/route/add/CmdConfigProtocolStaticRouteAdd.h"
5556
#include "fboss/cli/fboss2/commands/config/ptp/CmdConfigPtp.h"
@@ -480,29 +481,39 @@ const CommandTree& kConfigCommandTree() {
480481
argRegistrar<
481482
CmdConfigProtocolBgpPolicyRoutingPolicyTermTraits>,
482483
{{
483-
"action",
484-
"Configure a term action",
485-
{{
486-
"result",
487-
"Set the term result: "
488-
"<ACCEPT|REJECT|CONTINUE>",
489-
commandHandler<
490-
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionResult>,
491-
argRegistrar<
492-
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionResultTraits>,
493-
},
494-
{
495-
"set",
496-
"Set a route attribute: "
497-
"as-path prepend|community|"
498-
"local-pref|med|next-hop|"
499-
"origin|weight <value> ...",
500-
commandHandler<
501-
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionSet>,
502-
argRegistrar<
503-
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionSetTraits>,
504-
}},
505-
}},
484+
"action",
485+
"Configure a term action",
486+
{{
487+
"result",
488+
"Set the term result: "
489+
"<ACCEPT|REJECT|CONTINUE>",
490+
commandHandler<
491+
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionResult>,
492+
argRegistrar<
493+
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionResultTraits>,
494+
},
495+
{
496+
"set",
497+
"Set a route attribute: "
498+
"as-path prepend|community|"
499+
"local-pref|med|next-hop|"
500+
"origin|weight <value> ...",
501+
commandHandler<
502+
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionSet>,
503+
argRegistrar<
504+
CmdConfigProtocolBgpPolicyRoutingPolicyTermActionSetTraits>,
505+
}},
506+
},
507+
{
508+
"match",
509+
"Configure a term match: "
510+
"from as-path-list|origin|"
511+
"prefix-list <value>",
512+
commandHandler<
513+
CmdConfigProtocolBgpPolicyRoutingPolicyTermMatch>,
514+
argRegistrar<
515+
CmdConfigProtocolBgpPolicyRoutingPolicyTermMatchTraits>,
516+
}},
506517
}},
507518
}},
508519
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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

Comments
 (0)