diff options
Diffstat (limited to '')
-rw-r--r-- | tests/lib/test_grpc.cpp | 979 |
1 files changed, 979 insertions, 0 deletions
diff --git a/tests/lib/test_grpc.cpp b/tests/lib/test_grpc.cpp new file mode 100644 index 0000000..88372f4 --- /dev/null +++ b/tests/lib/test_grpc.cpp @@ -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 void grpc_thread_stop(struct thread *thread); + +static void _err_print(const void *cookie, const char *errstr) +{ + std::cout << "Failed to load grpc module:" << errstr << std::endl; +} + +static void static_startup(void) +{ + // struct frrmod_runtime module; + // static struct option_chain *oc; + + 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(), 0, 0); + if (!grpc_module) { + modpath = std::string(binpath) + std::string("../../lib"); + grpc_module = frrmod_load("grpc:50051", modpath.c_str(), + _err_print, 0); + } + if (!grpc_module) + 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 void 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); +} + +static void 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"; |