From fe56018a1d81d5301de47cbf7303251057c572fc Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 8 Jul 2024 12:09:21 -0700 Subject: [PATCH] sync routes (#303) * Sync Routes can add a new route from config service If a route does not exist locally, we will insert it, and fetch all of its parts. DevaddrRanges, Skfs, Euis. * handle removing routes that are left over after syncing * move syncing routes to cli module - add cli config test suite - pretty print the added and removed ids or routes The diffing for the routes is done in the cli module, this way the route stream worker doesn't start to become like the xor filter worker where it started to hold the kitchen sink. Routes are only sent to the route stream worker after they have been inserted and need their parts updated. * add modules for org list request and response --- config/ct.config | 1 + src/cli/hpr_cli_config.erl | 148 ++++++++- src/grpc/iot_config/hpr_org.erl | 56 ++++ src/grpc/iot_config/hpr_org_list_req.erl | 11 + src/grpc/iot_config/hpr_org_list_res.erl | 36 +++ src/grpc/iot_config/hpr_route_list_req.erl | 58 ++++ src/grpc/iot_config/hpr_route_list_res.erl | 13 + test/hpr_cli_config_SUITE.erl | 342 +++++++++++++++++++++ test/hpr_route_stream_worker_SUITE.erl | 2 + test/hpr_test_ics_org_service.erl | 55 ++++ test/hpr_test_ics_route_service.erl | 31 +- 11 files changed, 750 insertions(+), 3 deletions(-) create mode 100644 src/grpc/iot_config/hpr_org.erl create mode 100644 src/grpc/iot_config/hpr_org_list_req.erl create mode 100644 src/grpc/iot_config/hpr_org_list_res.erl create mode 100644 src/grpc/iot_config/hpr_route_list_req.erl create mode 100644 src/grpc/iot_config/hpr_route_list_res.erl create mode 100644 test/hpr_cli_config_SUITE.erl create mode 100644 test/hpr_test_ics_org_service.erl diff --git a/config/ct.config b/config/ct.config index 8f053532..4f2695bb 100644 --- a/config/ct.config +++ b/config/ct.config @@ -72,6 +72,7 @@ grpc_opts => #{ service_protos => [iot_config_pb], services => #{ + 'helium.iot_config.org' => hpr_test_ics_org_service, 'helium.iot_config.gateway' => hpr_test_ics_gateway_service, 'helium.iot_config.route' => hpr_test_ics_route_service } diff --git a/src/cli/hpr_cli_config.erl b/src/cli/hpr_cli_config.erl index 55c16182..6b1708ad 100644 --- a/src/cli/hpr_cli_config.erl +++ b/src/cli/hpr_cli_config.erl @@ -6,8 +6,23 @@ -behavior(clique_handler). +-include("hpr.hrl"). + -export([register_cli/0]). +-export([ + get_api_routes/0, + get_api_routes_for_oui/1 +]). + +-ifdef(TEST). + +-export([ + config_route_sync/3 +]). + +-endif. + register_cli() -> register_all_usage(), register_all_cmds(). @@ -42,9 +57,11 @@ config_usage() -> " [--display_skfs] default: false (SKFs not included)\n", "config route refresh_all - Refresh all routes\n", " [--minimum] default: 1 (need a minimum of 1 SKFs ro be updated)\n", - "config route refresh - Refresh route\n", + "config route refresh - Refresh route's EUIs, SKFs, DevAddrRanges\n", "config route activate - Activate route\n", "config route deactivate - Deactivate route\n", + "config route remove - Delete all remnants of a route\n", + "config route sync [--oui=] - Fetch all Routes from Config Service, creating new, removing old\n" "config skf - List all Session Key Filters for Devaddr or Session Key\n", "config eui --app --dev - List all Routes with EUI pair\n" "\n\n", @@ -104,6 +121,18 @@ config_cmd() -> [], fun config_route_deactivate/3 ], + [ + ["config", "route", "remove", '*'], + [], + [], + fun config_route_remove/3 + ], + [ + ["config", "route", "sync"], + [], + [{oui, [{shortname, "o"}, {longname, "oui"}]}], + fun config_route_sync/3 + ], [["config", "skf", '*'], [], [], fun config_skf/3], [ ["config", "eui"], @@ -398,6 +427,45 @@ config_route_deactivate(["config", "route", "deactivate", RouteID], [], _Flags) config_route_deactivate(_, _, _) -> usage. +config_route_remove(["config", "route", "remove", RouteID], [], []) -> + case hpr_route_storage:lookup(RouteID) of + {ok, RouteETS} -> + Route = hpr_route_ets:route(RouteETS), + hpr_route_storage:delete(Route), + c_text("Deleted Route: ~p", [RouteID]); + {error, not_found} -> + c_text("Could not find ~p", [RouteID]) + end; +config_route_remove(_, _, _) -> + usage. + +config_route_sync(["config", "route", "sync"], [], Flags) -> + Updates = + case maps:from_list(Flags) of + #{oui := OUI0} -> + OUI = erlang:list_to_integer(OUI0, 10), + APIRoutes = get_api_routes_for_oui(OUI), + ExistingRoutes = hpr_route_storage:oui_routes(OUI), + sync_routes(APIRoutes, ExistingRoutes); + #{} -> + APIRoutes = get_api_routes(), + ExistingRoutes = hpr_route_storage:all_routes(), + sync_routes(APIRoutes, ExistingRoutes) + end, + + FormatRoute = fun(Route) -> + io_lib:format(" - ~s OUI=~p~n", [hpr_route:id(Route), hpr_route:oui(Route)]) + end, + Added = lists:map(FormatRoute, maps:get(added, Updates, [])), + Removed = lists:map(FormatRoute, maps:get(removed, Updates, [])), + + c_list( + [io_lib:format("=== Added (~p) ===~n", [length(Added)])] ++ Added ++ + [io_lib:format("=== Removed (~p) ===~n", [length(Removed)])] ++ Removed + ); +config_route_sync(_, _, _) -> + usage. + config_skf(["config", "skf", DevAddrOrSKF], [], []) -> SKFS = case hpr_utils:hex_to_bin(erlang:list_to_binary(DevAddrOrSKF)) of @@ -728,3 +796,81 @@ format_skf({{SKF, DevAddr}, MaxCopies}) -> hpr_utils:bin_to_hex_string(SKF), MaxCopies ]). + +-spec get_api_routes() -> list(hpr_route:route()). +get_api_routes() -> + {ok, OrgList, _Meta} = helium_iot_config_org_client:list( + hpr_org_list_req:new(), + #{channel => ?IOT_CONFIG_CHANNEL} + ), + + lists:flatmap( + fun(OUI) -> get_api_routes_for_oui(OUI) end, + hpr_org_list_res:org_ouis(OrgList) + ). + +-spec get_api_routes_for_oui(OUI :: non_neg_integer()) -> list(hpr_route:route()). +get_api_routes_for_oui(OUI) -> + PubKeyBin = hpr_utils:pubkey_bin(), + SigFun = hpr_utils:sig_fun(), + + ListReq = hpr_route_list_req:new(PubKeyBin, OUI), + SignedReq = hpr_route_list_req:sign(ListReq, SigFun), + {ok, RouteListRes, _Meta} = helium_iot_config_route_client:list( + SignedReq, + #{channel => ?IOT_CONFIG_CHANNEL} + ), + hpr_route_list_res:routes(RouteListRes). + +-spec sync_routes( + APIRoutes :: list(hpr_route:route()), + ExistingRoutes :: list(hpr_route:route()) +) -> #{added => list(hpr_route:route()), removed => list(hpr_route:route())}. +sync_routes(APIRoutes, ExistingRoutes) -> + sync_routes(APIRoutes, ExistingRoutes, #{added => [], removed => []}). + +-spec sync_routes( + APIRoutes :: list(hpr_route:route()), + ExistingRoutes :: list(hpr_route:route()), + Updates :: map() +) -> #{added => list(hpr_route:route()), removed => list(hpr_route:route())}. +sync_routes([], [], Updates) -> + Updates; +sync_routes([], [Route | LeftoverRoutes], #{removed := RemovedRoutes} = Updates) -> + RouteID = hpr_route:id(Route), + lager:info([{route_id, RouteID}], "removing leftover route: ~p"), + ok = hpr_route_storage:delete(Route), + sync_routes( + [], + LeftoverRoutes, + Updates#{removed => [Route | RemovedRoutes]} + ); +sync_routes([Route | Routes], ExistingRoutes, #{added := AddedRoutes} = Updates) -> + RouteID = hpr_route:id(Route), + case hpr_route_storage:lookup(RouteID) of + {ok, _Route} -> + lager:info([{route_id, RouteID}], "doing nothing, route already exists"), + sync_routes( + Routes, + remove_route(Route, ExistingRoutes), + Updates + ); + {error, not_found} -> + lager:info([{route_id, RouteID}], "syncing new route"), + ok = hpr_route_storage:insert(Route), + hpr_route_stream_worker:refresh_route(hpr_route:id(Route)), + + sync_routes( + Routes, + remove_route(Route, ExistingRoutes), + Updates#{added => [Route | AddedRoutes]} + ) + end. + +-spec remove_route( + Target :: hpr_route:route(), + Coll :: list(hpr_route:route()) +) -> list(hpr_route:route()). +remove_route(Target, RouteList) -> + ID = hpr_route:id(Target), + lists:filter(fun(R) -> hpr_route:id(R) =/= ID end, RouteList). diff --git a/src/grpc/iot_config/hpr_org.erl b/src/grpc/iot_config/hpr_org.erl new file mode 100644 index 00000000..abe1b078 --- /dev/null +++ b/src/grpc/iot_config/hpr_org.erl @@ -0,0 +1,56 @@ +-module(hpr_org). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + oui/1, + owner/1, + payer/1, + delegate_keys/1, + locked/1 +]). + +-type org() :: #iot_config_org_v1_pb{}. + +-ifdef(TEST). + +-export([test_new/1]). + +-endif. + +-spec oui(Org :: org()) -> non_neg_integer(). +oui(Org) -> + Org#iot_config_org_v1_pb.oui. + +-spec owner(Org :: org()) -> binary(). +owner(Org) -> + Org#iot_config_org_v1_pb.owner. + +-spec payer(Org :: org()) -> binary(). +payer(Org) -> + Org#iot_config_org_v1_pb.payer. + +-spec delegate_keys(Org :: org()) -> list(binary()). +delegate_keys(Org) -> + Org#iot_config_org_v1_pb.delegate_keys. + +-spec locked(Org :: org()) -> boolean(). +locked(Org) -> + Org#iot_config_org_v1_pb.locked. + +%% ------------------------------------------------------------------ +%% Tests Functions +%% ------------------------------------------------------------------ +-ifdef(TEST). + +-spec test_new(RouteMap :: map()) -> org(). +test_new(RouteMap) -> + #iot_config_org_v1_pb{ + oui = maps:get(oui, RouteMap), + owner = maps:get(owner, RouteMap, <<"owner-test-value">>), + payer = maps:get(payer, RouteMap, <<"payer-test-value">>), + delegate_keys = maps:get(delegate_keys, RouteMap, []), + locked = maps:get(locked, RouteMap, false) + }. + +-endif. diff --git a/src/grpc/iot_config/hpr_org_list_req.erl b/src/grpc/iot_config/hpr_org_list_req.erl new file mode 100644 index 00000000..e75fe8ed --- /dev/null +++ b/src/grpc/iot_config/hpr_org_list_req.erl @@ -0,0 +1,11 @@ +-module(hpr_org_list_req). + +-include("../autogen/iot_config_pb.hrl"). + +-export([new/0]). + +-type req() :: #iot_config_org_list_req_v1_pb{}. + +-spec new() -> req(). +new() -> + #iot_config_org_list_req_v1_pb{}. diff --git a/src/grpc/iot_config/hpr_org_list_res.erl b/src/grpc/iot_config/hpr_org_list_res.erl new file mode 100644 index 00000000..e5a7a427 --- /dev/null +++ b/src/grpc/iot_config/hpr_org_list_res.erl @@ -0,0 +1,36 @@ +-module(hpr_org_list_res). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + orgs/1, + timestamp/1, + signer/1, + signature/1 +]). + +-export([ + org_ouis/1 +]). + +-type res() :: #iot_config_org_list_res_v1_pb{}. + +-spec orgs(Res :: res()) -> list(hpr_org:org()). +orgs(Res) -> + Res#iot_config_org_list_res_v1_pb.orgs. + +-spec timestamp(Res :: res()) -> non_neg_integer(). +timestamp(Res) -> + Res#iot_config_org_list_res_v1_pb.timestamp. + +-spec signer(Res :: res()) -> binary(). +signer(Res) -> + Res#iot_config_org_list_res_v1_pb.signer. + +-spec signature(Res :: res()) -> binary(). +signature(Res) -> + Res#iot_config_org_list_res_v1_pb.signature. + +-spec org_ouis(Res :: res()) -> list(non_neg_integer()). +org_ouis(Res) -> + [hpr_org:oui(Org) || Org <- ?MODULE:orgs(Res)]. diff --git a/src/grpc/iot_config/hpr_route_list_req.erl b/src/grpc/iot_config/hpr_route_list_req.erl new file mode 100644 index 00000000..d52339a9 --- /dev/null +++ b/src/grpc/iot_config/hpr_route_list_req.erl @@ -0,0 +1,58 @@ +-module(hpr_route_list_req). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + new/2, + timestamp/1, + signer/1, + signature/1 +]). + +-export([ + sign/2, + verify/1 +]). + +-type req() :: #iot_config_route_list_req_v1_pb{}. + +-spec new(Signer :: libp2p_crypto:pubkey_bin(), Oui :: non_neg_integer()) -> req(). +new(Signer, Oui) -> + #iot_config_route_list_req_v1_pb{ + oui = Oui, + timestamp = erlang:system_time(millisecond), + signer = Signer + }. + +-spec timestamp(Req :: req()) -> non_neg_integer(). +timestamp(Req) -> + Req#iot_config_route_list_req_v1_pb.timestamp. + +-spec signer(Req:: req()) -> binary(). +signer(Req) -> + Req#iot_config_route_list_req_v1_pb.signer. + +-spec signature(Req:: req()) -> binary(). +signature(Req) -> + Req#iot_config_route_list_req_v1_pb.signature. + +-spec sign(RouteListReq :: req(), SigFun :: fun()) -> req(). +sign(RouteListReq, SigFun) -> + EncodedRouteListReq = iot_config_pb:encode_msg( + RouteListReq, iot_config_route_list_req_v1_pb + ), + RouteListReq#iot_config_route_list_req_v1_pb{signature = SigFun(EncodedRouteListReq)}. + +-spec verify(RouteListReq :: req()) -> boolean(). +verify(RouteListReq) -> + EncodedRouteListReq = iot_config_pb:encode_msg( + RouteListReq#iot_config_route_list_req_v1_pb{ + signature = <<>> + }, + iot_config_route_list_req_v1_pb + ), + libp2p_crypto:verify( + EncodedRouteListReq, + ?MODULE:signature(RouteListReq), + libp2p_crypto:bin_to_pubkey(?MODULE:signer(RouteListReq)) + ). diff --git a/src/grpc/iot_config/hpr_route_list_res.erl b/src/grpc/iot_config/hpr_route_list_res.erl new file mode 100644 index 00000000..96ab0b60 --- /dev/null +++ b/src/grpc/iot_config/hpr_route_list_res.erl @@ -0,0 +1,13 @@ +-module(hpr_route_list_res). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + routes/1 +]). + +-type res() :: #iot_config_route_list_res_v1_pb{}. + +-spec routes(Res :: res()) -> list(hpr_route:route()). +routes(Res) -> + Res#iot_config_route_list_res_v1_pb.routes. diff --git a/test/hpr_cli_config_SUITE.erl b/test/hpr_cli_config_SUITE.erl new file mode 100644 index 00000000..3f78341f --- /dev/null +++ b/test/hpr_cli_config_SUITE.erl @@ -0,0 +1,342 @@ +-module(hpr_cli_config_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-export([ + all/0, + init_per_testcase/2, + end_per_testcase/2 +]). + +-export([ + sync_new_route_test/1, + sync_remove_route_test/1, + sync_new_remove_route_test/1, + sync_oui_only_test/1 +]). + +%%-------------------------------------------------------------------- +%% COMMON TEST CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% Running tests for this suite +%% @end +%%-------------------------------------------------------------------- +all() -> + [ + sync_new_route_test, + sync_remove_route_test, + sync_new_remove_route_test, + sync_oui_only_test + ]. + +%%-------------------------------------------------------------------- +%% TEST CASE SETUP +%%-------------------------------------------------------------------- +init_per_testcase(TestCase, Config) -> + test_utils:init_per_testcase(TestCase, Config). + +%%-------------------------------------------------------------------- +%% TEST CASE TEARDOWN +%%-------------------------------------------------------------------- +end_per_testcase(TestCase, Config) -> + meck:unload(), + test_utils:end_per_testcase(TestCase, Config), + ok. + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +sync_oui_only_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + #{ + route_id := Route1ID, + route := Route1, + eui_pair := EUIPair1, + devaddr_range := DevAddrRange1, + skf := SessionKeyFilter1 + } = test_data("7d502f32-4d58-4746-965e-001"), + + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {route, Route1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {eui_pair, EUIPair1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {devaddr_range, DevAddrRange1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {skf, SessionKeyFilter1}}) + ), + ok = check_config_counts(Route1ID, 1, 1, 1, 1), + + %% Ensure a different route is setup to come back, removing the original, + %% and adding the new. + #{ + route_id := Route2ID, + route := Route2, + eui_pair := EUIPair2, + devaddr_range := DevAddrRange2, + skf := SessionKeyFilter2 + } = test_data("7d502f32-4d58-4746-965e-002", 2), + application:set_env(hpr, test_route_list, [Route2]), + application:set_env(hpr, test_route_get_euis, [EUIPair2]), + application:set_env(hpr, test_route_get_devaddr_ranges, [DevAddrRange2]), + application:set_env(hpr, test_route_list_skfs, [SessionKeyFilter2]), + + %% Syncing Routes removes the route that no longer exists + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], [ + {oui, "2"} + ]), + %% ct:print("~s", [_Output]), + + % Route 1 was not removed + ok = check_config_counts(Route1ID, 2, 2, 2, 1), + ok = check_config_counts(Route2ID, 2, 2, 2, 1), + + ok. + +sync_new_route_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + %% Route does not exist locally. It will be added when + %% hpr_route_stream_worker:sync_routes() is called. + #{ + route_id := RouteID, + route := Route, + eui_pair := EUIPair, + devaddr := DevAddr, + devaddr_range := DevAddrRange, + session_key := SessionKey, + skf := SessionKeyFilter + } = test_data("7d502f32-4d58-4746-965e-001"), + + ok = application:set_env(hpr, test_route_list, [Route]), + ok = application:set_env(hpr, test_route_get_euis, [EUIPair]), + ok = application:set_env(hpr, test_route_get_devaddr_ranges, [DevAddrRange]), + ok = application:set_env(hpr, test_route_list_skfs, [SessionKeyFilter]), + + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], []), + %% ct:print("~s", [_Output]), + {ok, RouteETS} = hpr_route_storage:lookup(RouteID), + + ?assertEqual([RouteETS], hpr_devaddr_range_storage:lookup(16#00000005)), + ?assertEqual([RouteETS], hpr_eui_pair_storage:lookup(1, 12)), + SK1 = hpr_utils:hex_to_bin(SessionKey), + SKFEts = hpr_route_ets:skf_ets(RouteETS), + ?assertEqual([{SK1, 1}], hpr_skf_storage:lookup(SKFEts, DevAddr)), + + ok. + +sync_new_remove_route_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + #{ + route_id := Route1ID, + route := Route1, + eui_pair := EUIPair1, + devaddr := DevAddr1, + devaddr_range := DevAddrRange1, + skf := SessionKeyFilter1 + } = test_data("7d502f32-4d58-4746-965e-001"), + + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {route, Route1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {eui_pair, EUIPair1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {devaddr_range, DevAddrRange1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {skf, SessionKeyFilter1}}) + ), + ok = check_config_counts(Route1ID, 1, 1, 1, 1), + + %% Ensure a different route is setup to come back, removing the original, + %% and adding the new. + #{ + route_id := Route2ID, + route := Route2, + eui_pair := EUIPair2, + devaddr := DevAddr2, + devaddr_range := DevAddrRange2, + skf := SessionKeyFilter2 + } = test_data("7d502f32-4d58-4746-965e-002", 2), + application:set_env(hpr, test_route_list, [Route2]), + application:set_env(hpr, test_route_get_euis, [EUIPair2]), + application:set_env(hpr, test_route_get_devaddr_ranges, [DevAddrRange2]), + application:set_env(hpr, test_route_list_skfs, [SessionKeyFilter2]), + + %% Syncing Routes removes the route that no longer exists + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], []), + %% ct:print("~s", [_Output]), + ok = check_config_counts(Route2ID, 1, 1, 1, 1), + + ?assertEqual({error, not_found}, hpr_route_storage:lookup(Route1ID)), + %% New route uses same values as the old one, except for ID. + {ok, RouteETS2} = hpr_route_storage:lookup(Route2ID), + ?assertEqual([RouteETS2], hpr_devaddr_range_storage:lookup(DevAddr1)), + ?assertEqual([RouteETS2], hpr_eui_pair_storage:lookup(1, 12)), + + SK2_0 = hpr_skf:session_key(SessionKeyFilter2), + SK2 = hpr_utils:hex_to_bin(SK2_0), + SKFEts = hpr_route_ets:skf_ets(RouteETS2), + ?assertEqual([{SK2, 1}], hpr_skf_storage:lookup(SKFEts, DevAddr2)), + + ok. + +sync_remove_route_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + Route1ID = "7d502f32-4d58-4746-965e-001", + Route1 = hpr_route:test_new(#{ + id => Route1ID, + net_id => 0, + oui => 1, + server => #{ + host => "localhost", + port => 8080, + protocol => {packet_router, #{}} + }, + max_copies => 10 + }), + EUIPair1 = hpr_eui_pair:test_new(#{route_id => Route1ID, app_eui => 1, dev_eui => 0}), + DevAddrRange1 = hpr_devaddr_range:test_new(#{ + route_id => Route1ID, start_addr => 16#00000001, end_addr => 16#0000000A + }), + DevAddr1 = 16#00000001, + SessionKey1 = hpr_utils:bin_to_hex_string(crypto:strong_rand_bytes(16)), + SessionKeyFilter1 = hpr_skf:new(#{ + route_id => Route1ID, devaddr => DevAddr1, session_key => SessionKey1, max_copies => 1 + }), + + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {route, Route1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {eui_pair, EUIPair1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {devaddr_range, DevAddrRange1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {skf, SessionKeyFilter1}}) + ), + ok = check_config_counts(Route1ID, 1, 1, 1, 1), + + %% Ensure nothing is meant to come back from the config service when syncing. + application:set_env(hpr, test_route_list, []), + application:set_env(hpr, test_route_get_euis, []), + application:set_env(hpr, test_route_get_devaddr_ranges, []), + application:set_env(hpr, test_route_list_skfs, []), + + %% Syncing Routes removes the route that no longer exists + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], []), + %% ct:print("~s", [_Output]), + ?assertEqual({error, not_found}, hpr_route_storage:lookup(Route1ID)), + ?assertEqual([], hpr_devaddr_range_storage:lookup(DevAddr1)), + ?assertEqual([], hpr_eui_pair_storage:lookup(1, 12)), + ?assertEqual([], hpr_skf_storage:lookup_route(Route1ID)), + + ok. + +%% =================================================================== +%% Helpers +%% =================================================================== + +test_data(RouteID) -> + test_data(RouteID, 1). + +test_data(RouteID, OUI) -> + Route1 = hpr_route:test_new(#{ + id => RouteID, + net_id => 0, + oui => OUI, + server => #{ + host => "localhost", + port => 8080, + protocol => {packet_router, #{}} + }, + max_copies => 10 + }), + EUIPair1 = hpr_eui_pair:test_new(#{ + route_id => RouteID, app_eui => 1, dev_eui => 0 + }), + DevAddrRange1 = hpr_devaddr_range:test_new(#{ + route_id => RouteID, start_addr => 16#00000001, end_addr => 16#0000000A + }), + DevAddr1 = 16#00000001, + SessionKey1 = hpr_utils:bin_to_hex_string(crypto:strong_rand_bytes(16)), + SessionKeyFilter1 = hpr_skf:new(#{ + route_id => RouteID, + devaddr => DevAddr1, + session_key => SessionKey1, + max_copies => 1 + }), + #{ + route_id => RouteID, + route => Route1, + eui_pair => EUIPair1, + devaddr => DevAddr1, + devaddr_range => DevAddrRange1, + session_key => SessionKey1, + skf => SessionKeyFilter1 + }. + +check_config_counts( + RouteID, + ExpectedRouteCount, + ExpectedEUIPairCount, + ExpectedDevaddrRangeCount, + %% NOTE: SKF are separated by Route, provide amount expected for RouteID + ExpectedSKFCount +) -> + ok = test_utils:wait_until( + fun() -> + case hpr_route_storage:lookup(RouteID) of + {ok, RouteETS} -> + RouteCount = ets:info(hpr_routes_ets, size), + EUIPairCount = ets:info(hpr_route_eui_pairs_ets, size), + DevaddrRangeCount = ets:info(hpr_route_devaddr_ranges_ets, size), + SKFCount = ets:info(hpr_route_ets:skf_ets(RouteETS), size), + + { + ExpectedRouteCount =:= RouteCount andalso + ExpectedEUIPairCount =:= EUIPairCount andalso + ExpectedDevaddrRangeCount =:= DevaddrRangeCount andalso + ExpectedSKFCount =:= SKFCount, + [ + {route_id, RouteID}, + {route, ExpectedRouteCount, RouteCount}, + {eui_pair, ExpectedEUIPairCount, EUIPairCount}, + {devaddr_range, ExpectedDevaddrRangeCount, DevaddrRangeCount}, + {skf, ExpectedSKFCount, SKFCount}, + {skf_items, ets:tab2list(hpr_route_ets:skf_ets(RouteETS))} + ] + }; + _ -> + {false, {route_not_found, RouteID}, + {all_routes, [hpr_route_storage:all_routes()]}} + end + end + ). diff --git a/test/hpr_route_stream_worker_SUITE.erl b/test/hpr_route_stream_worker_SUITE.erl index d2859d44..8e248279 100644 --- a/test/hpr_route_stream_worker_SUITE.erl +++ b/test/hpr_route_stream_worker_SUITE.erl @@ -725,7 +725,9 @@ test_data(RouteID) -> route_id => RouteID, route => Route1, eui_pair => EUIPair1, + devaddr => DevAddr1, devaddr_range => DevAddrRange1, + session_key => SessionKey1, skf => SessionKeyFilter1 }. diff --git a/test/hpr_test_ics_org_service.erl b/test/hpr_test_ics_org_service.erl new file mode 100644 index 00000000..aa6cbbba --- /dev/null +++ b/test/hpr_test_ics_org_service.erl @@ -0,0 +1,55 @@ +-module(hpr_test_ics_org_service). + +-behaviour(helium_iot_config_org_bhvr). + +-include("../src/grpc/autogen/iot_config_pb.hrl"). + +-export([ + init/2, + handle_info/2 +]). + +-export([ + list/2, + get/2, + create_helium/2, + create_roamer/2, + update/2, + disable/2, + enable/2 +]). + +-spec init(atom(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). +init(_RPC, StreamState) -> + StreamState. + +-spec handle_info(Msg :: any(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). +handle_info(_Msg, StreamState) -> + StreamState. + +list(Ctx, _Msg) -> + OrgList = #iot_config_org_list_res_v1_pb{ + orgs = application:get_env(hpr, test_org_service_orgs, []), + timestamp = erlang:system_time(millisecond), + signer = <<>>, + signature = <<>> + }, + {ok, OrgList, Ctx}. + +get(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +create_helium(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +create_roamer(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +update(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +disable(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +enable(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. diff --git a/test/hpr_test_ics_route_service.erl b/test/hpr_test_ics_route_service.erl index b9cbe6fa..51e6f747 100644 --- a/test/hpr_test_ics_route_service.erl +++ b/test/hpr_test_ics_route_service.erl @@ -67,8 +67,35 @@ handle_info({stream_resp, RouteStreamResp}, StreamState) -> handle_info(_Msg, StreamState) -> StreamState. -list(_Ctx, _Msg) -> - {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. +list(Ctx, RouteListReq) -> + Encoded = iot_config_pb:encode_msg(RouteListReq#iot_config_route_list_req_v1_pb{ + signature = <<>> + }), + try + libp2p_crypto:verify( + Encoded, + RouteListReq#iot_config_route_list_req_v1_pb.signature, + libp2p_crypto:bin_to_pubkey(RouteListReq#iot_config_route_list_req_v1_pb.signer) + ) + of + false -> + {grpc_error, {grpcbox_stream:code_to_status(7), <<"PERMISSION_DENIED">>}}; + true -> + Oui = RouteListReq#iot_config_route_list_req_v1_pb.oui, + TestRoutes = application:get_env(hpr, test_route_list, []), + OuiRoutes = lists:filter(fun(Route) -> hpr_route:oui(Route) == Oui end, TestRoutes), + RouteList = #iot_config_route_list_res_v1_pb{ + routes = OuiRoutes, + timestamp = erlang:system_time(millisecond), + signer = <<>>, + signature = <<>> + }, + {ok, RouteList, Ctx} + catch + A:B:C -> + ct:print("hpr test route service list route error: ~n~p~n~p~n~p", [A, B, C]), + {grpc_error, {grpcbox_stream:code_to_status(7), <<"PERMISSION_DENIED">>}} + end. get(_Ctx, _Msg) -> {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}.