]> git.proxmox.com Git - mirror_frr.git/commitdiff
tests: add grpc unit test
authorChristian Hopps <chopps@labn.net>
Thu, 20 May 2021 23:22:14 +0000 (19:22 -0400)
committerChristian Hopps <chopps@gmail.com>
Sun, 6 Jun 2021 18:03:17 +0000 (18:03 +0000)
Test uses staticd which required some C++ header protections.
Additionally, the test also runs in the ubuntu20 docker container as
grpc is supported there by the packaging system.

Signed-off-by: Christian Hopps <chopps@labn.net>
14 files changed:
docker/ubuntu20-ci/Dockerfile
lib/routing_nb.h
lib/zebra.h
staticd/static_debug.h
staticd/static_nb.h
staticd/static_nht.h
staticd/static_routes.h
staticd/static_vrf.h
staticd/static_vty.h
staticd/static_zebra.c
staticd/static_zebra.h
tests/lib/test_grpc.cpp [new file with mode: 0644]
tests/lib/test_grpc.py [new file with mode: 0644]
tests/subdir.am

index 71fde305e6913961117da2c3b1c98c3e2183f58f..8b7557db1dc187be67b124897a06ebd986c2f092 100644 (file)
@@ -11,6 +11,7 @@ RUN apt update && \
       install-info build-essential libsystemd-dev libsnmp-dev perl \
       libcap-dev python2 libelf-dev \
       sudo gdb curl iputils-ping time \
+      libgrpc++-dev libgrpc-dev protobuf-compiler-grpc \
       mininet iproute2 iperf && \
       curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output /tmp/get-pip.py && \
       python2 /tmp/get-pip.py && \
@@ -57,6 +58,7 @@ RUN cd ~/frr && \
        --sbindir=/usr/lib/frr \
        --sysconfdir=/etc/frr \
        --enable-vtysh \
+       --enable-grpc \
        --enable-pimd \
        --enable-sharpd \
        --enable-multipath=64 \
index bdd12b262b0ec4d42d554f041b474417f988d5bd..c185091a4bce114bf471621cf55ccf7c7c04f8bb 100644 (file)
@@ -1,6 +1,10 @@
 #ifndef _FRR_ROUTING_NB_H_
 #define _FRR_ROUTING_NB_H_
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 extern const struct frr_yang_module_info frr_routing_info;
 
 /* Mandatory callbacks. */
@@ -28,4 +32,8 @@ DECLARE_HOOK(routing_conf_event, (struct nb_cb_create_args *args), (args));
 
 void routing_control_plane_protocols_register_vrf_dependency(void);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif /* _FRR_ROUTING_NB_H_ */
index 3b624117de769cbd45ae0c3a3e96389e30bc3485..6a02dcb922c8c6afb82d66f6136ae0f7a36895c0 100644 (file)
 #define static_cast(l, r) (r)
 #endif
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifndef HAVE_STRLCAT
 size_t strlcat(char *__restrict dest,
               const char *__restrict src, size_t destsize);
@@ -379,4 +383,8 @@ typedef uint32_t route_tag_t;
 #define ROUTE_TAG_MAX UINT32_MAX
 #define ROUTE_TAG_PRI PRIu32
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif /* _ZEBRA_H */
index 3a96339f47d8e977d62b47a1a4356c9460493bfe..ee9f7b053737ab82e165bbf5453f2fdd906848c2 100644 (file)
 #ifndef _STATIC_DEBUG_H
 #define _STATIC_DEBUG_H
 
-
 #include <zebra.h>
 
 #include "lib/debug.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /* staticd debugging records */
 extern struct debug static_dbg_events;
 extern struct debug static_dbg_route;
@@ -70,5 +73,8 @@ int static_debug_status_write(struct vty *vty);
  */
 void static_debug_set(int vtynode, bool onoff, bool events, bool route);
 
+#ifdef __cplusplus
+}
+#endif
 
 #endif /* _STATIC_DEBUG_H */
index 0313ae1c3af7dc8c637200ff595776f95908b9d4..5c3030fcfa5d4bd848971991dff6293121cdc63c 100644 (file)
 #ifndef _FRR_STATIC_NB_H_
 #define _FRR_STATIC_NB_H_
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 extern const struct frr_yang_module_info frr_staticd_info;
 
 /* Mandatory callbacks. */
@@ -181,4 +185,8 @@ int routing_control_plane_protocols_name_validate(
        FRR_S_ROUTE_SRC_INFO_KEY_NO_DISTANCE_XPATH                             \
        FRR_STATIC_ROUTE_NH_KEY_XPATH
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
index 9139c367d18d17225edc0e23549b0e7b9c6b6d72..08dba2ebb53f8d04422196ea6225a83ec82f6524 100644 (file)
 #ifndef __STATIC_NHT_H__
 #define __STATIC_NHT_H__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /*
  * When we get notification that nexthop tracking has an answer for
  * us call this function to find the nexthop we are tracking so it
@@ -53,4 +57,9 @@ extern void static_nht_mark_state(struct prefix *sp, vrf_id_t vrf_id,
  */
 extern void static_get_nh_str(struct static_nexthop *nh, char *nexthop,
                              size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
 #endif
index f64a40329d466c8424fd0503a729e9976f49ca4d..1269621cc9dbf05bb4a93217223d5684e9b11812 100644 (file)
 #include "table.h"
 #include "memory.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 DECLARE_MGROUP(STATIC);
 
 /* Static route label information */
@@ -216,4 +220,9 @@ extern void zebra_stable_node_cleanup(struct route_table *table,
  */
 extern void static_get_nh_str(struct static_nexthop *nh, char *nexthop,
                              size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+
 #endif
index 3977899054a061d0a6d114f1232d60a5dc170f7e..be311af8c44263ddd1c9be04bb3c8d833b8329f0 100644 (file)
 #ifndef __STATIC_VRF_H__
 #define __STATIC_VRF_H__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 struct static_vrf {
        struct vrf *vrf;
 
@@ -43,4 +47,8 @@ struct route_table *static_vrf_static_table(afi_t afi, safi_t safi,
                                            struct static_vrf *svrf);
 extern void static_vrf_terminate(void);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
index 7ffc8d9c98b3756b3bc1d5897f5690a74bdce4d3..01577685e5e07b74c9c564326660c162e863728f 100644 (file)
 #ifndef __STATIC_VTY_H__
 #define __STATIC_VTY_H__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 int static_config(struct vty *vty, struct static_vrf *svrf,
                  afi_t afi, safi_t safi, const char *cmd);
 
 void static_vty_init(void);
+
+#ifdef __cplusplus
+}
+#endif
+
 #endif
index 19c578c60e2edcf37ad75a6d614643a0c298a367..40275908f7fb0f0537d61e08d48e9a9e3d3b55ff 100644 (file)
@@ -529,6 +529,16 @@ void static_zebra_init(void)
                                      "Static Nexthop Tracking hash");
 }
 
+/* static_zebra_stop used by tests/lib/test_grpc.cpp */
+void static_zebra_stop(void)
+{
+       if (!zclient)
+               return;
+       zclient_stop(zclient);
+       zclient_free(zclient);
+       zclient = NULL;
+}
+
 void static_zebra_vrf_register(struct vrf *vrf)
 {
        if (vrf->vrf_id == VRF_DEFAULT)
index 9f93f3ee631084d7b4c83b48d2f5ff69198e935f..ca6308559e2e88b34b6de7893059f3d78092a571 100644 (file)
 #ifndef __STATIC_ZEBRA_H__
 #define __STATIC_ZEBRA_H__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 extern struct thread_master *master;
 
 extern void static_zebra_nht_register(struct route_node *rn,
@@ -28,9 +32,15 @@ extern void static_zebra_route_add(struct route_node *rn,
                                   struct static_path *pn, safi_t safi,
                                   bool install);
 extern void static_zebra_init(void);
+/* static_zebra_stop used by tests/lib/test_grpc.cpp */
+extern void static_zebra_stop(void);
 extern void static_zebra_vrf_register(struct vrf *vrf);
 extern void static_zebra_vrf_unregister(struct vrf *vrf);
 extern int static_zebra_nh_update(struct route_node *rn,
                                  struct static_nexthop *nh);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/tests/lib/test_grpc.cpp b/tests/lib/test_grpc.cpp
new file mode 100644 (file)
index 0000000..4917968
--- /dev/null
@@ -0,0 +1,979 @@
+/*
+ * May 16 2021, Christian Hopps <chopps@labn.net>
+ *
+ * Copyright (c) 2021, LabN Consulting, L.L.C
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; see the file COPYING; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <time.h>
+#include <unistd.h>
+#include <zebra.h>
+
+#include "filter.h"
+#include "frr_pthread.h"
+#include "libfrr.h"
+#include "routing_nb.h"
+#include "northbound_cli.h"
+#include "thread.h"
+#include "vrf.h"
+#include "vty.h"
+
+#include "staticd/static_debug.h"
+#include "staticd/static_nb.h"
+#include "staticd/static_vrf.h"
+#include "staticd/static_vty.h"
+#include "staticd/static_zebra.h"
+
+// GRPC C++ includes
+#include <string>
+#include <sstream>
+#include <grpc/grpc.h>
+#include <grpcpp/channel.h>
+#include <grpcpp/client_context.h>
+#include <grpcpp/create_channel.h>
+#include <grpcpp/security/credentials.h>
+#include "grpc/frr-northbound.grpc.pb.h"
+
+DEFINE_HOOK(frr_late_init, (struct thread_master * tm), (tm));
+DEFINE_KOOH(frr_fini, (), ());
+
+struct vty *vty;
+
+bool mpls_enabled;
+struct thread_master *master;
+struct zebra_privs_t static_privs = {0};
+struct frrmod_runtime *grpc_module;
+char binpath[2 * MAXPATHLEN + 1];
+
+extern const char *json_expect1;
+extern const char *json_expect2;
+extern const char *json_expect3;
+extern const char *json_loadconf1;
+
+int test_dbg = 1;
+
+void inline test_debug(const std::string &s)
+{
+       if (test_dbg)
+               std::cout << s << std::endl;
+}
+
+// static struct option_chain modules[] = {{ .arg = "grpc:50051" }]
+// static struct option_chain **modnext = modules->next;
+
+static const struct frr_yang_module_info *const staticd_yang_modules[] = {
+       &frr_interface_info, &frr_filter_info, &frr_routing_info,
+       &frr_staticd_info,   &frr_vrf_info,
+};
+
+static int grpc_thread_stop(struct thread *thread);
+
+static void static_startup(void)
+{
+       // struct frrmod_runtime module;
+       // static struct option_chain *oc;
+       char moderr[256] = {};
+       cmd_init(1);
+
+       zlog_aux_init("NONE: ", LOG_DEBUG);
+       zprivs_preinit(&static_privs);
+       zprivs_init(&static_privs);
+
+       /* Load the server side module -- check libtool path first */
+       std::string modpath = std::string(binpath) + std::string("../../../lib/.libs");
+       grpc_module = frrmod_load("grpc:50051", modpath.c_str(), moderr, sizeof(moderr));
+       if (!grpc_module) {
+               modpath = std::string(binpath) +  std::string("../../lib");
+               grpc_module = frrmod_load("grpc:50051", modpath.c_str(), moderr,
+                                         sizeof(moderr));
+       }
+       if (!grpc_module) {
+               std::cout << "Failed to load grpc module:" << moderr
+                         << std::endl;
+               exit(1);
+       }
+
+       static_debug_init();
+
+       master = thread_master_create(NULL);
+       nb_init(master, staticd_yang_modules, array_size(staticd_yang_modules),
+               false);
+
+       static_zebra_init();
+       vty_init(master, true);
+       static_vrf_init();
+       static_vty_init();
+
+       hook_register(routing_conf_event,
+                     routing_control_plane_protocols_name_validate);
+
+       routing_control_plane_protocols_register_vrf_dependency();
+
+       // Add a route
+       vty = vty_new();
+       vty->type = vty::VTY_TERM;
+       vty_config_enter(vty, true, false);
+
+       auto ret = cmd_execute(vty, "ip route 11.0.0.0/8 Null0", NULL, 0);
+       assert(!ret);
+
+       ret = cmd_execute(vty, "end", NULL, 0);
+       assert(!ret);
+
+       nb_cli_pending_commit_check(vty);
+
+       frr_pthread_init();
+
+       // frr_config_fork();
+       hook_call(frr_late_init, master);
+}
+
+static void static_shutdown(void)
+{
+       hook_call(frr_fini);
+       vty_close(vty);
+       vrf_terminate();
+       vty_terminate();
+       cmd_terminate();
+       nb_terminate();
+       yang_terminate();
+       thread_master_free(master);
+       master = NULL;
+}
+
+using frr::Northbound;
+using grpc::Channel;
+using grpc::ClientAsyncResponseReader;
+using grpc::ClientContext;
+using grpc::CompletionQueue;
+using grpc::Status;
+
+class NorthboundClient
+{
+      public:
+       NorthboundClient(std::shared_ptr<Channel> channel)
+           : stub_(frr::Northbound::NewStub(channel))
+       {
+       }
+
+       void Commit(uint32_t candidate_id)
+       {
+               frr::CommitRequest request;
+               frr::CommitResponse reply;
+               ClientContext context;
+               Status status;
+
+               request.set_candidate_id(candidate_id);
+
+               request.set_phase(frr::CommitRequest::ALL);
+               status = stub_->Commit(&context, request, &reply);
+               _throw_if_not_ok(status);
+#if 0
+               request.set_phase(frr::CommitRequest::VALIDATE);
+               status = stub_->Commit(&context, request, &reply);
+               _throw_if_not_ok(status);
+
+               request.set_phase(frr::CommitRequest::PREPARE);
+               status = stub_->Commit(&context, request, &reply);
+               _throw_if_not_ok(status);
+
+               request.set_phase(frr::CommitRequest::APPLY);
+               status = stub_->Commit(&context, request, &reply);
+               _throw_if_not_ok(status);
+#endif
+       }
+
+       uint32_t CreateCandidate()
+       {
+               frr::CreateCandidateRequest request;
+               frr::CreateCandidateResponse reply;
+               ClientContext context;
+               Status status;
+
+               status = stub_->CreateCandidate(&context, request, &reply);
+               _throw_if_not_ok(status);
+               return reply.candidate_id();
+       }
+
+       void DeleteCandidate(uint32_t candidate_id)
+       {
+               frr::DeleteCandidateRequest request;
+               frr::DeleteCandidateResponse reply;
+               ClientContext context;
+               Status status;
+
+               request.set_candidate_id(candidate_id);
+               status = stub_->DeleteCandidate(&context, request, &reply);
+               _throw_if_not_ok(status);
+       }
+
+       void EditCandidate(uint32_t candidate_id, const std::string &path,
+                          const std::string &value)
+       {
+               frr::EditCandidateRequest request;
+               frr::EditCandidateResponse reply;
+               ClientContext context;
+
+               request.set_candidate_id(candidate_id);
+               frr::PathValue *pv = request.add_update();
+               pv->set_path(path);
+               pv->set_value(value);
+
+               Status status = stub_->EditCandidate(&context, request, &reply);
+               _throw_if_not_ok(status);
+       }
+
+       std::string Get(const std::string &path,
+                       frr::GetRequest::DataType dtype, frr::Encoding enc,
+                       bool with_defaults)
+       {
+               frr::GetRequest request;
+               frr::GetResponse reply;
+               ClientContext context;
+               std::ostringstream ss;
+
+               request.set_type(dtype);
+               request.set_encoding(enc);
+               request.set_with_defaults(with_defaults);
+               request.add_path(path);
+
+               auto stream = stub_->Get(&context, request);
+               while (stream->Read(&reply)) {
+                       ss << reply.data().data() << std::endl;
+               }
+               auto status = stream->Finish();
+               _throw_if_not_ok(status);
+               return ss.str();
+       }
+
+       std::string GetCapabilities()
+       {
+               frr::GetCapabilitiesRequest request;
+               frr::GetCapabilitiesResponse reply;
+               ClientContext context;
+
+               Status status =
+                       stub_->GetCapabilities(&context, request, &reply);
+               _throw_if_not_ok(status);
+
+               std::ostringstream ss;
+               ss << "Capabilities:" << std::endl
+                  << "\tVersion: " << reply.frr_version() << std::endl
+                  << "\tRollback Support: " << reply.rollback_support()
+                  << std::endl
+                  << "\tSupported Modules:";
+
+               for (int i = 0; i < reply.supported_modules_size(); i++) {
+                       auto sm = reply.supported_modules(i);
+                       ss << std::endl
+                          << "\t\tName: \"" << sm.name()
+                          << "\" Revision: " << sm.revision() << " Org: \""
+                          << sm.organization() << "\"";
+               }
+
+               ss << std::endl << "\tSupported Encodings:";
+
+               for (int i = 0; i < reply.supported_encodings_size(); i++) {
+                       auto se = reply.supported_encodings(i);
+                       auto desc =
+                               google::protobuf::GetEnumDescriptor<decltype(
+                                       se)>();
+                       ss << std::endl
+                          << "\t\t" << desc->FindValueByNumber(se)->name();
+               }
+
+               ss << std::endl;
+
+               return ss.str();
+       }
+
+       void LoadToCandidate(uint32_t candidate_id, bool is_replace,
+                            bool is_json, const std::string &data)
+       {
+               frr::LoadToCandidateRequest request;
+               frr::LoadToCandidateResponse reply;
+               frr::DataTree *dt = new frr::DataTree;
+               ClientContext context;
+
+               request.set_candidate_id(candidate_id);
+               request.set_type(is_replace
+                                        ? frr::LoadToCandidateRequest::REPLACE
+                                        : frr::LoadToCandidateRequest::MERGE);
+               dt->set_encoding(is_json ? frr::JSON : frr::XML);
+               dt->set_data(data);
+               request.set_allocated_config(dt);
+
+               Status status =
+                       stub_->LoadToCandidate(&context, request, &reply);
+               _throw_if_not_ok(status);
+       }
+
+       std::string ListTransactions()
+       {
+               frr::ListTransactionsRequest request;
+               frr::ListTransactionsResponse reply;
+               ClientContext context;
+               std::ostringstream ss;
+
+               auto stream = stub_->ListTransactions(&context, request);
+
+               while (stream->Read(&reply)) {
+                       ss << "Tx ID: " << reply.id()
+                          << " client: " << reply.client()
+                          << " date: " << reply.date()
+                          << " comment: " << reply.comment() << std::endl;
+               }
+
+               auto status = stream->Finish();
+               _throw_if_not_ok(status);
+               return ss.str();
+       }
+
+      private:
+       std::unique_ptr<frr::Northbound::Stub> stub_;
+
+       void _throw_if_not_ok(Status &status)
+       {
+               if (!status.ok())
+                       throw std::runtime_error(
+                               std::to_string(status.error_code()) + ": "
+                               + status.error_message());
+       }
+};
+
+
+bool stop = false;
+
+int grpc_client_test_stop(struct frr_pthread *fpt, void **result)
+{
+       test_debug("client: STOP pthread");
+
+       assert(fpt->running);
+       atomic_store_explicit(&fpt->running, false, memory_order_relaxed);
+
+       test_debug("client: joining pthread");
+       pthread_join(fpt->thread, result);
+
+       test_debug("client: joined pthread");
+       return 0;
+}
+
+int find_first_diff(const std::string &s1, const std::string &s2)
+{
+       int s1len = s1.length();
+       int s2len = s2.length();
+       int mlen = std::min(s1len, s2len);
+
+       for (int i = 0; i < mlen; i++)
+               if (s1[i] != s2[i])
+                       return i;
+       return s1len == s2len ? -1 : mlen;
+}
+
+void assert_no_diff(const std::string &s1, const std::string &s2)
+{
+       int pos = find_first_diff(s1, s2);
+       if (pos == -1)
+               return;
+       std::cout << "not ok" << std::endl;
+       std::cout << "Same: " << s1.substr(0, pos) << std::endl;
+       std::cout << "Diff s1: " << s1.substr(pos) << std::endl;
+       std::cout << "Diff s2: " << s2.substr(pos) << std::endl;
+       assert(false);
+}
+
+void assert_config_same(NorthboundClient &client, const std::string &compare)
+{
+       std::string confs = client.Get("/frr-routing:routing",
+                                      frr::GetRequest::ALL, frr::JSON, true);
+       assert_no_diff(confs, compare);
+       std::cout << "ok" << std::endl;
+}
+
+void grpc_client_run_test(void)
+{
+       NorthboundClient client(grpc::CreateChannel(
+               "localhost:50051", grpc::InsecureChannelCredentials()));
+
+       std::string reply = client.GetCapabilities();
+
+       uint32_t cid;
+       cid = client.CreateCandidate();
+       std::cout << "CreateCandidate -> " << cid << std::endl;
+       assert(cid == 1);
+       client.DeleteCandidate(cid);
+       std::cout << "DeleteCandidate(" << cid << ")" << std::endl;
+       cid = client.CreateCandidate();
+       assert(cid == 2);
+       std::cout << "CreateCandidate -> " << cid << std::endl;
+
+       /*
+        * Get initial configuration
+        */
+       std::cout << "Comparing initial config...";
+       assert_config_same(client, json_expect1);
+
+       /*
+        * Add config using EditCandidate
+        */
+
+       char xpath_buf[1024];
+       strlcpy(xpath_buf,
+               "/frr-routing:routing/control-plane-protocols/"
+               "control-plane-protocol[type='frr-staticd:staticd']"
+               "[name='staticd'][vrf='default']/frr-staticd:staticd/route-list",
+               sizeof(xpath_buf));
+       int slen = strlen(xpath_buf);
+       for (int i = 0; i < 4; i++) {
+               snprintf(xpath_buf + slen, sizeof(xpath_buf) - slen,
+                        "[prefix='13.0.%d.0/24']"
+                        "[afi-safi='frr-routing:ipv4-unicast']/"
+                        "path-list[table-id='0'][distance='1']/"
+                        "frr-nexthops/nexthop[nh-type='blackhole']"
+                        "[vrf='default'][gateway=''][interface='(null)']",
+                        i);
+               client.EditCandidate(cid, xpath_buf, "");
+       }
+       client.Commit(cid);
+       std::cout << "Comparing EditCandidate config...";
+       assert_config_same(client, json_expect2);
+
+       client.DeleteCandidate(cid);
+       std::cout << "DeleteCandidate(" << cid << ")" << std::endl;
+
+       /*
+        * Add config using LoadToCandidate
+        */
+
+       cid = client.CreateCandidate();
+       std::cout << "CreateCandidate -> " << cid << std::endl;
+
+       client.LoadToCandidate(cid, false, true, json_loadconf1);
+       client.Commit(cid);
+
+       std::cout << "Comparing LoadToCandidate config...";
+       assert_config_same(client, json_expect3);
+
+       client.DeleteCandidate(cid);
+       std::cout << "DeleteCandidate(" << cid << ")" << std::endl;
+
+       std::string ltxreply = client.ListTransactions();
+       // std::cout << "client: pthread received: " << ltxreply << std::endl;
+}
+
+void *grpc_client_test_start(void *arg)
+{
+       struct frr_pthread *fpt = (struct frr_pthread *)arg;
+       fpt->master->owner = pthread_self();
+       frr_pthread_set_name(fpt);
+       frr_pthread_notify_running(fpt);
+
+       try {
+               grpc_client_run_test();
+               std::cout << "TEST PASSED" << std::endl;
+       } catch (std::exception &e) {
+               std::cout << "Exception in test: " << e.what() << std::endl;
+       }
+
+       // Signal FRR event loop to stop
+       test_debug("client: pthread: adding event to stop us");
+       thread_add_event(master, grpc_thread_stop, NULL, 0, NULL);
+
+       test_debug("client: pthread: DONE (returning)");
+
+       return NULL;
+}
+
+static int grpc_thread_start(struct thread *thread)
+{
+       struct frr_pthread_attr client = {
+               .start = grpc_client_test_start,
+               .stop = grpc_client_test_stop,
+       };
+
+       auto pth = frr_pthread_new(&client, "GRPC Client thread", "grpc");
+       frr_pthread_run(pth, NULL);
+       frr_pthread_wait_running(pth);
+
+       return 0;
+}
+
+static int grpc_thread_stop(struct thread *thread)
+{
+       std::cout << __func__ << ": frr_pthread_stop_all" << std::endl;
+       frr_pthread_stop_all();
+       std::cout << __func__ << ": static_shutdown" << std::endl;
+       static_shutdown();
+       std::cout << __func__ << ": exit cleanly" << std::endl;
+       exit(0);
+}
+
+/*
+ * return abs path to this binary with trailing `/`. Does not parse path
+ * environment to find in path, which should not matter for unit testing.
+ */
+static int get_binpath(const char *argv0, char cwd[2 * MAXPATHLEN + 1])
+{
+       const char *rch;
+       if (argv0[0] == '/') {
+               *cwd = 0;
+               rch = strrchr(argv0, '/');
+               strlcpy(cwd, argv0, MIN(rch - argv0 + 2, 2 * MAXPATHLEN + 1));
+               return 0;
+       }
+       if (!(rch = strrchr(argv0, '/'))) {
+               /* Does not handle using PATH, shouldn't matter for test */
+               errno = EINVAL;
+               return -1;
+       }
+       if (!getcwd(cwd, MAXPATHLEN))
+               return -1;
+       int len = strlen(cwd);
+       cwd[len++] = '/';
+       strlcpy(cwd + len, argv0, MIN(rch - argv0 + 2, 2 * MAXPATHLEN + 1));
+       return 0;
+}
+
+int main(int argc, char **argv)
+{
+       assert(argc >= 1);
+       if (get_binpath(argv[0], binpath) < 0)
+               exit(1);
+
+       static_startup();
+
+       thread_add_event(master, grpc_thread_start, NULL, 0, NULL);
+
+       /* Event Loop */
+       struct thread thread;
+       while (thread_fetch(master, &thread))
+               thread_call(&thread);
+       return 0;
+}
+
+// clang-format off
+
+const char *json_expect1 = R"NONCE({
+  "frr-routing:routing": {
+    "control-plane-protocols": {
+      "control-plane-protocol": [
+        {
+          "type": "frr-staticd:staticd",
+          "name": "staticd",
+          "vrf": "default",
+          "frr-staticd:staticd": {
+            "route-list": [
+              {
+                "prefix": "11.0.0.0/8",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      ]
+    }
+  },
+  "frr-vrf:lib": {
+    "vrf": [
+      {
+        "name": "default",
+        "state": {
+          "active": false
+        }
+      }
+    ]
+  }
+}
+
+)NONCE";
+
+const char *json_loadconf1 = R"NONCE(
+{
+  "frr-routing:routing": {
+    "control-plane-protocols": {
+      "control-plane-protocol": [
+        {
+          "type": "frr-staticd:staticd",
+          "name": "staticd",
+          "vrf": "default",
+          "frr-staticd:staticd": {
+            "route-list": [
+              {
+                "prefix": "10.0.0.0/13",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)"
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      ]
+    }
+  },
+  "frr-vrf:lib": {
+    "vrf": [
+      {
+        "name": "default"
+      }
+    ]
+  }
+})NONCE";
+
+const char *json_expect2 = R"NONCE({
+  "frr-routing:routing": {
+    "control-plane-protocols": {
+      "control-plane-protocol": [
+        {
+          "type": "frr-staticd:staticd",
+          "name": "staticd",
+          "vrf": "default",
+          "frr-staticd:staticd": {
+            "route-list": [
+              {
+                "prefix": "11.0.0.0/8",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.0.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.1.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.2.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.3.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      ]
+    }
+  },
+  "frr-vrf:lib": {
+    "vrf": [
+      {
+        "name": "default",
+        "state": {
+          "active": false
+        }
+      }
+    ]
+  }
+}
+
+)NONCE";
+
+const char *json_expect3 = R"NONCE({
+  "frr-routing:routing": {
+    "control-plane-protocols": {
+      "control-plane-protocol": [
+        {
+          "type": "frr-staticd:staticd",
+          "name": "staticd",
+          "vrf": "default",
+          "frr-staticd:staticd": {
+            "route-list": [
+              {
+                "prefix": "11.0.0.0/8",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.0.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.1.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.2.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "13.0.3.0/24",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              },
+              {
+                "prefix": "10.0.0.0/13",
+                "afi-safi": "frr-routing:ipv4-unicast",
+                "path-list": [
+                  {
+                    "table-id": 0,
+                    "distance": 1,
+                    "tag": 0,
+                    "frr-nexthops": {
+                      "nexthop": [
+                        {
+                          "nh-type": "blackhole",
+                          "vrf": "default",
+                          "gateway": "",
+                          "interface": "(null)",
+                          "bh-type": "null",
+                          "onlink": false
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      ]
+    }
+  },
+  "frr-vrf:lib": {
+    "vrf": [
+      {
+        "name": "default",
+        "state": {
+          "active": false
+        }
+      }
+    ]
+  }
+}
+
+)NONCE";
diff --git a/tests/lib/test_grpc.py b/tests/lib/test_grpc.py
new file mode 100644 (file)
index 0000000..06ae6c0
--- /dev/null
@@ -0,0 +1,23 @@
+import inspect
+import os
+import subprocess
+import pytest
+import frrtest
+
+class TestGRPC(object):
+    program = "./test_grpc"
+
+    @pytest.mark.skipif(
+        'S["GRPC_TRUE"]=""\n' not in open("../config.status").readlines(),
+        reason="GRPC not enabled",
+    )
+    def test_exits_cleanly(self):
+        basedir = os.path.dirname(inspect.getsourcefile(type(self)))
+        program = os.path.join(basedir, self.program)
+        proc = subprocess.Popen(
+            [frrtest.binpath(program)], stdin=subprocess.PIPE, stdout=subprocess.PIPE
+        )
+        output, _ = proc.communicate()
+        self.exitcode = proc.wait()
+        if self.exitcode != 0:
+            raise frrtest.TestExitNonzero(self)
index 39966997745e80bbd8092aab63f16bf28ec966c9..ca477851e393199b82fc163b0cf12a303efa0143 100644 (file)
@@ -106,6 +106,12 @@ check_PROGRAMS = \
        $(TESTS_ZEBRA) \
        # end
 
+if GRPC
+check_PROGRAMS += \
+       tests/lib/test_grpc \
+       #end
+endif
+
 if ZEROMQ
 check_PROGRAMS += \
        tests/lib/test_zmq \
@@ -156,9 +162,19 @@ TESTS_CFLAGS = \
        # end
 # note no -Werror
 
+TESTS_CXXFLAGS = \
+       $(AC_CXXFLAGS) \
+       $(LIBYANG_CFLAGS) \
+       $(SAN_FLAGS) \
+       # end
+# note no -Werror
+
 ALL_TESTS_LDADD = lib/libfrr.la $(LIBCAP)
 BGP_TEST_LDADD = bgpd/libbgp.a $(RFPLDADD) $(ALL_TESTS_LDADD) $(LIBYANG_LIBS) -lm
 ISISD_TEST_LDADD = isisd/libisis.a $(ALL_TESTS_LDADD)
+if GRPC
+GRPC_TESTS_LDADD = staticd/libstatic.a grpc/libfrrgrpc_pb.la -lgrpc++ -lprotobuf $(ALL_TESTS_LDADD) $(LIBYANG_LIBS) -lm
+endif
 OSPFD_TEST_LDADD = ospfd/libfrrospf.a $(ALL_TESTS_LDADD)
 OSPF6_TEST_LDADD = ospf6d/libospf6.a $(ALL_TESTS_LDADD)
 ZEBRA_TEST_LDADD = zebra/label_manager.o $(ALL_TESTS_LDADD)
@@ -251,6 +267,12 @@ tests_lib_northbound_test_oper_data_CPPFLAGS = $(TESTS_CPPFLAGS)
 tests_lib_northbound_test_oper_data_LDADD = $(ALL_TESTS_LDADD)
 tests_lib_northbound_test_oper_data_SOURCES = tests/lib/northbound/test_oper_data.c
 nodist_tests_lib_northbound_test_oper_data_SOURCES = yang/frr-test-module.yang.c
+if GRPC
+tests_lib_test_grpc_CXXFLAGS = $(WERROR) $(TESTS_CXXFLAGS)
+tests_lib_test_grpc_CPPFLAGS = $(TESTS_CPPFLAGS)
+tests_lib_test_grpc_LDADD = $(GRPC_TESTS_LDADD)
+tests_lib_test_grpc_SOURCES = tests/lib/test_grpc.cpp
+endif
 tests_lib_test_assert_CFLAGS = $(TESTS_CFLAGS)
 tests_lib_test_assert_CPPFLAGS = $(TESTS_CPPFLAGS)
 tests_lib_test_assert_LDADD = $(ALL_TESTS_LDADD)