1 // SPDX-License-Identifier: GPL-2.0-or-later
3 * May 16 2021, Christian Hopps <chopps@labn.net>
5 * Copyright (c) 2021, LabN Consulting, L.L.C
13 #include "frr_pthread.h"
15 #include "routing_nb.h"
16 #include "northbound_cli.h"
21 #include "staticd/static_debug.h"
22 #include "staticd/static_nb.h"
23 #include "staticd/static_vrf.h"
24 #include "staticd/static_vty.h"
25 #include "staticd/static_zebra.h"
30 #include <grpc/grpc.h>
31 #include <grpcpp/channel.h>
32 #include <grpcpp/client_context.h>
33 #include <grpcpp/create_channel.h>
34 #include <grpcpp/security/credentials.h>
35 #include "grpc/frr-northbound.grpc.pb.h"
37 DEFINE_HOOK(frr_late_init
, (struct thread_master
* tm
), (tm
));
38 DEFINE_KOOH(frr_fini
, (), ());
43 struct thread_master
*master
;
44 struct zebra_privs_t static_privs
= {0};
45 struct frrmod_runtime
*grpc_module
;
46 char binpath
[2 * MAXPATHLEN
+ 1];
48 extern const char *json_expect1
;
49 extern const char *json_expect2
;
50 extern const char *json_expect3
;
51 extern const char *json_loadconf1
;
55 void inline test_debug(const std::string
&s
)
58 std::cout
<< s
<< std::endl
;
61 // static struct option_chain modules[] = {{ .arg = "grpc:50051" }]
62 // static struct option_chain **modnext = modules->next;
64 static const struct frr_yang_module_info
*const staticd_yang_modules
[] = {
65 &frr_interface_info
, &frr_filter_info
, &frr_routing_info
,
66 &frr_staticd_info
, &frr_vrf_info
,
69 static void grpc_thread_stop(struct thread
*thread
);
71 static void _err_print(const void *cookie
, const char *errstr
)
73 std::cout
<< "Failed to load grpc module:" << errstr
<< std::endl
;
76 static void static_startup(void)
78 // struct frrmod_runtime module;
79 // static struct option_chain *oc;
83 zlog_aux_init("NONE: ", LOG_DEBUG
);
84 zprivs_preinit(&static_privs
);
85 zprivs_init(&static_privs
);
87 /* Load the server side module -- check libtool path first */
88 std::string modpath
= std::string(binpath
) + std::string("../../../lib/.libs");
89 grpc_module
= frrmod_load("grpc:50051", modpath
.c_str(), 0, 0);
91 modpath
= std::string(binpath
) + std::string("../../lib");
92 grpc_module
= frrmod_load("grpc:50051", modpath
.c_str(),
100 master
= thread_master_create(NULL
);
101 nb_init(master
, staticd_yang_modules
, array_size(staticd_yang_modules
),
105 vty_init(master
, true);
109 hook_register(routing_conf_event
,
110 routing_control_plane_protocols_name_validate
);
112 routing_control_plane_protocols_register_vrf_dependency();
116 vty
->type
= vty::VTY_TERM
;
117 vty_config_enter(vty
, true, false);
119 auto ret
= cmd_execute(vty
, "ip route 11.0.0.0/8 Null0", NULL
, 0);
122 ret
= cmd_execute(vty
, "end", NULL
, 0);
125 nb_cli_pending_commit_check(vty
);
129 // frr_config_fork();
130 hook_call(frr_late_init
, master
);
133 static void static_shutdown(void)
142 thread_master_free(master
);
146 using frr::Northbound
;
148 using grpc::ClientAsyncResponseReader
;
149 using grpc::ClientContext
;
150 using grpc::CompletionQueue
;
153 class NorthboundClient
156 NorthboundClient(std::shared_ptr
<Channel
> channel
)
157 : stub_(frr::Northbound::NewStub(channel
))
161 void Commit(uint32_t candidate_id
)
163 frr::CommitRequest request
;
164 frr::CommitResponse reply
;
165 ClientContext context
;
168 request
.set_candidate_id(candidate_id
);
170 request
.set_phase(frr::CommitRequest::ALL
);
171 status
= stub_
->Commit(&context
, request
, &reply
);
172 _throw_if_not_ok(status
);
174 request
.set_phase(frr::CommitRequest::VALIDATE
);
175 status
= stub_
->Commit(&context
, request
, &reply
);
176 _throw_if_not_ok(status
);
178 request
.set_phase(frr::CommitRequest::PREPARE
);
179 status
= stub_
->Commit(&context
, request
, &reply
);
180 _throw_if_not_ok(status
);
182 request
.set_phase(frr::CommitRequest::APPLY
);
183 status
= stub_
->Commit(&context
, request
, &reply
);
184 _throw_if_not_ok(status
);
188 uint32_t CreateCandidate()
190 frr::CreateCandidateRequest request
;
191 frr::CreateCandidateResponse reply
;
192 ClientContext context
;
195 status
= stub_
->CreateCandidate(&context
, request
, &reply
);
196 _throw_if_not_ok(status
);
197 return reply
.candidate_id();
200 void DeleteCandidate(uint32_t candidate_id
)
202 frr::DeleteCandidateRequest request
;
203 frr::DeleteCandidateResponse reply
;
204 ClientContext context
;
207 request
.set_candidate_id(candidate_id
);
208 status
= stub_
->DeleteCandidate(&context
, request
, &reply
);
209 _throw_if_not_ok(status
);
212 void EditCandidate(uint32_t candidate_id
, const std::string
&path
,
213 const std::string
&value
)
215 frr::EditCandidateRequest request
;
216 frr::EditCandidateResponse reply
;
217 ClientContext context
;
219 request
.set_candidate_id(candidate_id
);
220 frr::PathValue
*pv
= request
.add_update();
222 pv
->set_value(value
);
224 Status status
= stub_
->EditCandidate(&context
, request
, &reply
);
225 _throw_if_not_ok(status
);
228 std::string
Get(const std::string
&path
,
229 frr::GetRequest::DataType dtype
, frr::Encoding enc
,
232 frr::GetRequest request
;
233 frr::GetResponse reply
;
234 ClientContext context
;
235 std::ostringstream ss
;
237 request
.set_type(dtype
);
238 request
.set_encoding(enc
);
239 request
.set_with_defaults(with_defaults
);
240 request
.add_path(path
);
242 auto stream
= stub_
->Get(&context
, request
);
243 while (stream
->Read(&reply
)) {
244 ss
<< reply
.data().data() << std::endl
;
246 auto status
= stream
->Finish();
247 _throw_if_not_ok(status
);
251 std::string
GetCapabilities()
253 frr::GetCapabilitiesRequest request
;
254 frr::GetCapabilitiesResponse reply
;
255 ClientContext context
;
258 stub_
->GetCapabilities(&context
, request
, &reply
);
259 _throw_if_not_ok(status
);
261 std::ostringstream ss
;
262 ss
<< "Capabilities:" << std::endl
263 << "\tVersion: " << reply
.frr_version() << std::endl
264 << "\tRollback Support: " << reply
.rollback_support()
266 << "\tSupported Modules:";
268 for (int i
= 0; i
< reply
.supported_modules_size(); i
++) {
269 auto sm
= reply
.supported_modules(i
);
271 << "\t\tName: \"" << sm
.name()
272 << "\" Revision: " << sm
.revision() << " Org: \""
273 << sm
.organization() << "\"";
276 ss
<< std::endl
<< "\tSupported Encodings:";
278 for (int i
= 0; i
< reply
.supported_encodings_size(); i
++) {
279 auto se
= reply
.supported_encodings(i
);
281 google::protobuf::GetEnumDescriptor
<decltype(
284 << "\t\t" << desc
->FindValueByNumber(se
)->name();
292 void LoadToCandidate(uint32_t candidate_id
, bool is_replace
,
293 bool is_json
, const std::string
&data
)
295 frr::LoadToCandidateRequest request
;
296 frr::LoadToCandidateResponse reply
;
297 frr::DataTree
*dt
= new frr::DataTree
;
298 ClientContext context
;
300 request
.set_candidate_id(candidate_id
);
301 request
.set_type(is_replace
302 ? frr::LoadToCandidateRequest::REPLACE
303 : frr::LoadToCandidateRequest::MERGE
);
304 dt
->set_encoding(is_json
? frr::JSON
: frr::XML
);
306 request
.set_allocated_config(dt
);
309 stub_
->LoadToCandidate(&context
, request
, &reply
);
310 _throw_if_not_ok(status
);
313 std::string
ListTransactions()
315 frr::ListTransactionsRequest request
;
316 frr::ListTransactionsResponse reply
;
317 ClientContext context
;
318 std::ostringstream ss
;
320 auto stream
= stub_
->ListTransactions(&context
, request
);
322 while (stream
->Read(&reply
)) {
323 ss
<< "Tx ID: " << reply
.id()
324 << " client: " << reply
.client()
325 << " date: " << reply
.date()
326 << " comment: " << reply
.comment() << std::endl
;
329 auto status
= stream
->Finish();
330 _throw_if_not_ok(status
);
335 std::unique_ptr
<frr::Northbound::Stub
> stub_
;
337 void _throw_if_not_ok(Status
&status
)
340 throw std::runtime_error(
341 std::to_string(status
.error_code()) + ": "
342 + status
.error_message());
349 int grpc_client_test_stop(struct frr_pthread
*fpt
, void **result
)
351 test_debug("client: STOP pthread");
353 assert(fpt
->running
);
354 atomic_store_explicit(&fpt
->running
, false, memory_order_relaxed
);
356 test_debug("client: joining pthread");
357 pthread_join(fpt
->thread
, result
);
359 test_debug("client: joined pthread");
363 int find_first_diff(const std::string
&s1
, const std::string
&s2
)
365 int s1len
= s1
.length();
366 int s2len
= s2
.length();
367 int mlen
= std::min(s1len
, s2len
);
369 for (int i
= 0; i
< mlen
; i
++)
372 return s1len
== s2len
? -1 : mlen
;
375 void assert_no_diff(const std::string
&s1
, const std::string
&s2
)
377 int pos
= find_first_diff(s1
, s2
);
380 std::cout
<< "not ok" << std::endl
;
381 std::cout
<< "Same: " << s1
.substr(0, pos
) << std::endl
;
382 std::cout
<< "Diff s1: " << s1
.substr(pos
) << std::endl
;
383 std::cout
<< "Diff s2: " << s2
.substr(pos
) << std::endl
;
387 void assert_config_same(NorthboundClient
&client
, const std::string
&compare
)
389 std::string confs
= client
.Get("/frr-routing:routing",
390 frr::GetRequest::ALL
, frr::JSON
, true);
391 assert_no_diff(confs
, compare
);
392 std::cout
<< "ok" << std::endl
;
395 void grpc_client_run_test(void)
397 NorthboundClient
client(grpc::CreateChannel(
398 "localhost:50051", grpc::InsecureChannelCredentials()));
400 std::string reply
= client
.GetCapabilities();
403 cid
= client
.CreateCandidate();
404 std::cout
<< "CreateCandidate -> " << cid
<< std::endl
;
406 client
.DeleteCandidate(cid
);
407 std::cout
<< "DeleteCandidate(" << cid
<< ")" << std::endl
;
408 cid
= client
.CreateCandidate();
410 std::cout
<< "CreateCandidate -> " << cid
<< std::endl
;
413 * Get initial configuration
415 std::cout
<< "Comparing initial config...";
416 assert_config_same(client
, json_expect1
);
419 * Add config using EditCandidate
422 char xpath_buf
[1024];
424 "/frr-routing:routing/control-plane-protocols/"
425 "control-plane-protocol[type='frr-staticd:staticd']"
426 "[name='staticd'][vrf='default']/frr-staticd:staticd/route-list",
428 int slen
= strlen(xpath_buf
);
429 for (int i
= 0; i
< 4; i
++) {
430 snprintf(xpath_buf
+ slen
, sizeof(xpath_buf
) - slen
,
431 "[prefix='13.0.%d.0/24']"
432 "[afi-safi='frr-routing:ipv4-unicast']/"
433 "path-list[table-id='0'][distance='1']/"
434 "frr-nexthops/nexthop[nh-type='blackhole']"
435 "[vrf='default'][gateway=''][interface='(null)']",
437 client
.EditCandidate(cid
, xpath_buf
, "");
440 std::cout
<< "Comparing EditCandidate config...";
441 assert_config_same(client
, json_expect2
);
443 client
.DeleteCandidate(cid
);
444 std::cout
<< "DeleteCandidate(" << cid
<< ")" << std::endl
;
447 * Add config using LoadToCandidate
450 cid
= client
.CreateCandidate();
451 std::cout
<< "CreateCandidate -> " << cid
<< std::endl
;
453 client
.LoadToCandidate(cid
, false, true, json_loadconf1
);
456 std::cout
<< "Comparing LoadToCandidate config...";
457 assert_config_same(client
, json_expect3
);
459 client
.DeleteCandidate(cid
);
460 std::cout
<< "DeleteCandidate(" << cid
<< ")" << std::endl
;
462 std::string ltxreply
= client
.ListTransactions();
463 // std::cout << "client: pthread received: " << ltxreply << std::endl;
466 void *grpc_client_test_start(void *arg
)
468 struct frr_pthread
*fpt
= (struct frr_pthread
*)arg
;
469 fpt
->master
->owner
= pthread_self();
470 frr_pthread_set_name(fpt
);
471 frr_pthread_notify_running(fpt
);
474 grpc_client_run_test();
475 std::cout
<< "TEST PASSED" << std::endl
;
476 } catch (std::exception
&e
) {
477 std::cout
<< "Exception in test: " << e
.what() << std::endl
;
480 // Signal FRR event loop to stop
481 test_debug("client: pthread: adding event to stop us");
482 thread_add_event(master
, grpc_thread_stop
, NULL
, 0, NULL
);
484 test_debug("client: pthread: DONE (returning)");
489 static void grpc_thread_start(struct thread
*thread
)
491 struct frr_pthread_attr client
= {
492 .start
= grpc_client_test_start
,
493 .stop
= grpc_client_test_stop
,
496 auto pth
= frr_pthread_new(&client
, "GRPC Client thread", "grpc");
497 frr_pthread_run(pth
, NULL
);
498 frr_pthread_wait_running(pth
);
501 static void grpc_thread_stop(struct thread
*thread
)
503 std::cout
<< __func__
<< ": frr_pthread_stop_all" << std::endl
;
504 frr_pthread_stop_all();
505 std::cout
<< __func__
<< ": static_shutdown" << std::endl
;
507 std::cout
<< __func__
<< ": exit cleanly" << std::endl
;
512 * return abs path to this binary with trailing `/`. Does not parse path
513 * environment to find in path, which should not matter for unit testing.
515 static int get_binpath(const char *argv0
, char cwd
[2 * MAXPATHLEN
+ 1])
518 if (argv0
[0] == '/') {
520 rch
= strrchr(argv0
, '/');
521 strlcpy(cwd
, argv0
, MIN(rch
- argv0
+ 2, 2 * MAXPATHLEN
+ 1));
524 if (!(rch
= strrchr(argv0
, '/'))) {
525 /* Does not handle using PATH, shouldn't matter for test */
529 if (!getcwd(cwd
, MAXPATHLEN
))
531 int len
= strlen(cwd
);
533 strlcpy(cwd
+ len
, argv0
, MIN(rch
- argv0
+ 2, 2 * MAXPATHLEN
+ 1));
537 int main(int argc
, char **argv
)
540 if (get_binpath(argv
[0], binpath
) < 0)
545 thread_add_event(master
, grpc_thread_start
, NULL
, 0, NULL
);
548 struct thread thread
;
549 while (thread_fetch(master
, &thread
))
550 thread_call(&thread
);
556 const char *json_expect1
= R
"NONCE({
557 "frr
-routing
:routing
": {
558 "control
-plane
-protocols
": {
559 "control
-plane
-protocol
": [
561 "type
": "frr
-staticd
:staticd
",
564 "frr
-staticd
:staticd
": {
567 "prefix
": "11.0.0.0/8",
568 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
577 "nh
-type
": "blackhole
",
580 "interface
": "(null
)",
609 const char *json_loadconf1
= R
"NONCE(
611 "frr
-routing
:routing
": {
612 "control
-plane
-protocols
": {
613 "control
-plane
-protocol
": [
615 "type
": "frr
-staticd
:staticd
",
618 "frr
-staticd
:staticd
": {
621 "prefix
": "10.0.0.0/13",
622 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
630 "nh
-type
": "blackhole
",
633 "interface
": "(null
)"
655 const char *json_expect2
= R
"NONCE({
656 "frr
-routing
:routing
": {
657 "control
-plane
-protocols
": {
658 "control
-plane
-protocol
": [
660 "type
": "frr
-staticd
:staticd
",
663 "frr
-staticd
:staticd
": {
666 "prefix
": "11.0.0.0/8",
667 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
676 "nh
-type
": "blackhole
",
679 "interface
": "(null
)",
689 "prefix
": "13.0.0.0/24",
690 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
699 "nh
-type
": "blackhole
",
702 "interface
": "(null
)",
712 "prefix
": "13.0.1.0/24",
713 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
722 "nh
-type
": "blackhole
",
725 "interface
": "(null
)",
735 "prefix
": "13.0.2.0/24",
736 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
745 "nh
-type
": "blackhole
",
748 "interface
": "(null
)",
758 "prefix
": "13.0.3.0/24",
759 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
768 "nh
-type
": "blackhole
",
771 "interface
": "(null
)",
800 const char *json_expect3
= R
"NONCE({
801 "frr
-routing
:routing
": {
802 "control
-plane
-protocols
": {
803 "control
-plane
-protocol
": [
805 "type
": "frr
-staticd
:staticd
",
808 "frr
-staticd
:staticd
": {
811 "prefix
": "11.0.0.0/8",
812 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
821 "nh
-type
": "blackhole
",
824 "interface
": "(null
)",
834 "prefix
": "13.0.0.0/24",
835 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
844 "nh
-type
": "blackhole
",
847 "interface
": "(null
)",
857 "prefix
": "13.0.1.0/24",
858 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
867 "nh
-type
": "blackhole
",
870 "interface
": "(null
)",
880 "prefix
": "13.0.2.0/24",
881 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
890 "nh
-type
": "blackhole
",
893 "interface
": "(null
)",
903 "prefix
": "13.0.3.0/24",
904 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
913 "nh
-type
": "blackhole
",
916 "interface
": "(null
)",
926 "prefix
": "10.0.0.0/13",
927 "afi
-safi
": "frr
-routing
:ipv4
-unicast
",
936 "nh
-type
": "blackhole
",
939 "interface
": "(null
)",