From 04efa0f23b803acf0d451d75893ec26963135e54 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 1 Jul 2024 17:44:25 -0700 Subject: [PATCH 1/4] 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. --- config/ct.config | 1 + src/grpc/iot_config/hpr_org.erl | 72 +++++++++++++++++++ src/grpc/iot_config/hpr_route_list_req.erl | 40 +++++++++++ src/grpc/iot_config/hpr_route_list_res.erl | 13 ++++ .../iot_config/hpr_route_stream_worker.erl | 56 +++++++++++++++ test/hpr_route_stream_worker_SUITE.erl | 49 ++++++++++++- test/hpr_test_ics_org_service.erl | 55 ++++++++++++++ test/hpr_test_ics_route_service.erl | 31 +++++++- 8 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 src/grpc/iot_config/hpr_org.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_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/grpc/iot_config/hpr_org.erl b/src/grpc/iot_config/hpr_org.erl new file mode 100644 index 00000000..3f3a7c2b --- /dev/null +++ b/src/grpc/iot_config/hpr_org.erl @@ -0,0 +1,72 @@ +-module(hpr_org). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + list_req/0, + list_res_ouis/1 +]). + +-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. + +%% ------------------------------------------------------------------ +%% Org Service +%% ------------------------------------------------------------------ +-spec list_req() -> #iot_config_org_list_req_v1_pb{}. +list_req() -> + #iot_config_org_list_req_v1_pb{}. + +-spec list_res_ouis(#iot_config_org_list_res_v1_pb{}) -> [non_neg_integer()]. +list_res_ouis(#iot_config_org_list_res_v1_pb{orgs = Orgs}) -> + lists:map(fun(Org) -> ?MODULE:oui(Org) end, Orgs). + +%% ------------------------------------------------------------------ +%% 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_route_list_req.erl b/src/grpc/iot_config/hpr_route_list_req.erl new file mode 100644 index 00000000..544ab832 --- /dev/null +++ b/src/grpc/iot_config/hpr_route_list_req.erl @@ -0,0 +1,40 @@ +-module(hpr_route_list_req). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + new/2, + 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 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/src/grpc/iot_config/hpr_route_stream_worker.erl b/src/grpc/iot_config/hpr_route_stream_worker.erl index 1fb28f4a..0c1889c6 100644 --- a/src/grpc/iot_config/hpr_route_stream_worker.erl +++ b/src/grpc/iot_config/hpr_route_stream_worker.erl @@ -62,6 +62,8 @@ -export([ start_link/1, refresh_route/1, + get_all_routes/0, + sync_routes/0, checkpoint/0, schedule_checkpoint/0 ]). @@ -148,6 +150,14 @@ start_link(Args) -> refresh_route(RouteID) -> gen_server:call(?MODULE, {refresh_route, RouteID}, timer:seconds(300)). +-spec get_all_routes() -> ok. +get_all_routes() -> + gen_server:call(?MODULE, get_all_routes, timer:seconds(120)). + +-spec sync_routes() -> ok. +sync_routes() -> + gen_server:call(?MODULE, sync_routes, timer:seconds(120)). + -spec checkpoint() -> ok. checkpoint() -> gen_server:call(?MODULE, checkpoint). @@ -238,6 +248,52 @@ init(Args) -> last_timestamp = LastTimestamp }}. +do_get_all_routes() -> + {ok, OrgList, _Meta} = helium_iot_config_org_client:list( + hpr_org:list_req(), + #{channel => ?IOT_CONFIG_CHANNEL} + ), + + PubKeyBin = hpr_utils:pubkey_bin(), + SigFun = hpr_utils:sig_fun(), + + Res = lists:flatmap( + fun(Oui) -> + ListReq = hpr_route_list_req:new(PubKeyBin, Oui), + SignedReq = hpr_route_list_req:sign(ListReq, SigFun), + {ok, RouteListRes, _Meta2} = helium_iot_config_route_client:list( + SignedReq, + #{channel => ?IOT_CONFIG_CHANNEL} + ), + hpr_route_list_res:routes(RouteListRes) + end, + hpr_org:list_res_ouis(OrgList) + ), + Res. + +do_sync_all_routes([]) -> + ok; +do_sync_all_routes([Route | Routes]) -> + RouteID = hpr_route:id(Route), + case hpr_route_storage:lookup(RouteID) of + {ok, Route} -> + lager:info([{route_id, RouteID}], "doing nothing, route already exists"); + {error, not_found} -> + lager:info([{route_id, RouteID}], "syncing new route"), + hpr_route_storage:insert(Route), + refresh_devaddrs(RouteID), + refresh_euis(RouteID), + refresh_skfs(RouteID) + end, + do_sync_all_routes(Routes). + +handle_call(get_all_routes, _From, State) -> + Routes = do_get_all_routes(), + {reply, {ok, Routes}, State}; +handle_call(sync_routes, _From, State) -> + Routes = do_get_all_routes(), + do_sync_all_routes(Routes), + {reply, ok, State}; handle_call({refresh_route, RouteID}, _From, State) -> DevaddrResponse = refresh_devaddrs(RouteID), EUIResponse = refresh_euis(RouteID), diff --git a/test/hpr_route_stream_worker_SUITE.erl b/test/hpr_route_stream_worker_SUITE.erl index d2859d44..dc592734 100644 --- a/test/hpr_route_stream_worker_SUITE.erl +++ b/test/hpr_route_stream_worker_SUITE.erl @@ -17,7 +17,8 @@ reset_stream_test/1, reset_channel_test/1, app_restart_rehydrate_test/1, - route_remove_delete_skf_dets_test/1 + route_remove_delete_skf_dets_test/1, + sync_new_route_test/1 ]). %%-------------------------------------------------------------------- @@ -38,7 +39,8 @@ all() -> reset_stream_test, reset_channel_test, app_restart_rehydrate_test, - route_remove_delete_skf_dets_test + route_remove_delete_skf_dets_test, + sync_new_route_test ]. %%-------------------------------------------------------------------- @@ -542,6 +544,49 @@ main_test(_Config) -> 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}) + ]), + + RouteID = "7d502f32-4d58-4746-965e-001", + Route = hpr_route:test_new(#{ + id => RouteID, + net_id => 0, + oui => 1, + server => #{ + host => "localhost", + port => 8080, + protocol => {packet_router, #{}} + }, + max_copies => 10 + }), + EUIPair = hpr_eui_pair:test_new(#{route_id => RouteID, app_eui => 1, dev_eui => 0}), + DevAddrRange = hpr_devaddr_range:test_new(#{ + route_id => RouteID, start_addr => 16#00000001, end_addr => 16#0000000A + }), + DevAddr = 16#00000001, + SessionKey = hpr_utils:bin_to_hex_string(crypto:strong_rand_bytes(16)), + SessionKeyFilter = hpr_skf:new(#{ + route_id => RouteID, devaddr => DevAddr, session_key => SessionKey, max_copies => 1 + }), + 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]), + + ok = hpr_route_stream_worker:sync_routes(), + {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. + refresh_route_test(_Config) -> %% Create route and send them from server Route1ID = "7d502f32-4d58-4746-965e-001", 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">>}}. From 5c778f21c92966eb90a8a855d151e6f6f848974c Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 2 Jul 2024 09:26:03 -0700 Subject: [PATCH 2/4] handle removing routes that are left over after syncing --- .../iot_config/hpr_route_stream_worker.erl | 17 +++-- test/hpr_route_stream_worker_SUITE.erl | 65 ++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/grpc/iot_config/hpr_route_stream_worker.erl b/src/grpc/iot_config/hpr_route_stream_worker.erl index 0c1889c6..8aa05564 100644 --- a/src/grpc/iot_config/hpr_route_stream_worker.erl +++ b/src/grpc/iot_config/hpr_route_stream_worker.erl @@ -271,9 +271,13 @@ do_get_all_routes() -> ), Res. -do_sync_all_routes([]) -> +do_sync_all_routes([], []) -> ok; -do_sync_all_routes([Route | Routes]) -> +do_sync_all_routes([], [Route | LeftoverRoutes]) -> + ct:print("removing leftover route: ~p", [{route_id, hpr_route:id(Route)}]), + ok = hpr_route_storage:delete(Route), + do_sync_all_routes([], LeftoverRoutes); +do_sync_all_routes([Route | Routes], ExistingRoutes) -> RouteID = hpr_route:id(Route), case hpr_route_storage:lookup(RouteID) of {ok, Route} -> @@ -285,14 +289,19 @@ do_sync_all_routes([Route | Routes]) -> refresh_euis(RouteID), refresh_skfs(RouteID) end, - do_sync_all_routes(Routes). + do_sync_all_routes(Routes, remove_route(Route, ExistingRoutes)). + +remove_route(Target, RouteList) -> + ID = hpr_route:id(Target), + lists:filter(fun(R) -> hpr_route:id(R) =/= ID end, RouteList). handle_call(get_all_routes, _From, State) -> Routes = do_get_all_routes(), {reply, {ok, Routes}, State}; handle_call(sync_routes, _From, State) -> Routes = do_get_all_routes(), - do_sync_all_routes(Routes), + ExistingRoutes = hpr_route_storage:all_routes(), + do_sync_all_routes(Routes, ExistingRoutes), {reply, ok, State}; handle_call({refresh_route, RouteID}, _From, State) -> DevaddrResponse = refresh_devaddrs(RouteID), diff --git a/test/hpr_route_stream_worker_SUITE.erl b/test/hpr_route_stream_worker_SUITE.erl index dc592734..a0951341 100644 --- a/test/hpr_route_stream_worker_SUITE.erl +++ b/test/hpr_route_stream_worker_SUITE.erl @@ -18,7 +18,8 @@ reset_channel_test/1, app_restart_rehydrate_test/1, route_remove_delete_skf_dets_test/1, - sync_new_route_test/1 + sync_new_route_test/1, + sync_remove_route_test/1 ]). %%-------------------------------------------------------------------- @@ -40,7 +41,8 @@ all() -> reset_channel_test, app_restart_rehydrate_test, route_remove_delete_skf_dets_test, - sync_new_route_test + sync_new_route_test, + sync_remove_route_test ]. %%-------------------------------------------------------------------- @@ -550,6 +552,8 @@ sync_new_route_test(_Config) -> hpr_org:test_new(#{oui => 2}) ]), + %% Route does not exist locally. It will be added when + %% hpr_route_stream_worker:sync_routes() is called. RouteID = "7d502f32-4d58-4746-965e-001", Route = hpr_route:test_new(#{ id => RouteID, @@ -587,6 +591,63 @@ sync_new_route_test(_Config) -> 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 + ok = hpr_route_stream_worker:sync_routes(), + ?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. + refresh_route_test(_Config) -> %% Create route and send them from server Route1ID = "7d502f32-4d58-4746-965e-001", From 5da903b851d274e33078aa8ad38c3955fc2f690c Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 2 Jul 2024 13:45:37 -0700 Subject: [PATCH 3/4] 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. --- src/cli/hpr_cli_config.erl | 147 +++++++- .../iot_config/hpr_route_stream_worker.erl | 65 ---- test/hpr_cli_config_SUITE.erl | 342 ++++++++++++++++++ test/hpr_route_stream_worker_SUITE.erl | 112 +----- 4 files changed, 492 insertions(+), 174 deletions(-) create mode 100644 test/hpr_cli_config_SUITE.erl diff --git a/src/cli/hpr_cli_config.erl b/src/cli/hpr_cli_config.erl index 55c16182..438bb58f 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,44 @@ 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, Route} -> + 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 +795,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(), + #{channel => ?IOT_CONFIG_CHANNEL} + ), + + lists:flatmap( + fun(OUI) -> get_api_routes_for_oui(OUI) end, + hpr_org:list_res_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_route_stream_worker.erl b/src/grpc/iot_config/hpr_route_stream_worker.erl index 8aa05564..1fb28f4a 100644 --- a/src/grpc/iot_config/hpr_route_stream_worker.erl +++ b/src/grpc/iot_config/hpr_route_stream_worker.erl @@ -62,8 +62,6 @@ -export([ start_link/1, refresh_route/1, - get_all_routes/0, - sync_routes/0, checkpoint/0, schedule_checkpoint/0 ]). @@ -150,14 +148,6 @@ start_link(Args) -> refresh_route(RouteID) -> gen_server:call(?MODULE, {refresh_route, RouteID}, timer:seconds(300)). --spec get_all_routes() -> ok. -get_all_routes() -> - gen_server:call(?MODULE, get_all_routes, timer:seconds(120)). - --spec sync_routes() -> ok. -sync_routes() -> - gen_server:call(?MODULE, sync_routes, timer:seconds(120)). - -spec checkpoint() -> ok. checkpoint() -> gen_server:call(?MODULE, checkpoint). @@ -248,61 +238,6 @@ init(Args) -> last_timestamp = LastTimestamp }}. -do_get_all_routes() -> - {ok, OrgList, _Meta} = helium_iot_config_org_client:list( - hpr_org:list_req(), - #{channel => ?IOT_CONFIG_CHANNEL} - ), - - PubKeyBin = hpr_utils:pubkey_bin(), - SigFun = hpr_utils:sig_fun(), - - Res = lists:flatmap( - fun(Oui) -> - ListReq = hpr_route_list_req:new(PubKeyBin, Oui), - SignedReq = hpr_route_list_req:sign(ListReq, SigFun), - {ok, RouteListRes, _Meta2} = helium_iot_config_route_client:list( - SignedReq, - #{channel => ?IOT_CONFIG_CHANNEL} - ), - hpr_route_list_res:routes(RouteListRes) - end, - hpr_org:list_res_ouis(OrgList) - ), - Res. - -do_sync_all_routes([], []) -> - ok; -do_sync_all_routes([], [Route | LeftoverRoutes]) -> - ct:print("removing leftover route: ~p", [{route_id, hpr_route:id(Route)}]), - ok = hpr_route_storage:delete(Route), - do_sync_all_routes([], LeftoverRoutes); -do_sync_all_routes([Route | Routes], ExistingRoutes) -> - RouteID = hpr_route:id(Route), - case hpr_route_storage:lookup(RouteID) of - {ok, Route} -> - lager:info([{route_id, RouteID}], "doing nothing, route already exists"); - {error, not_found} -> - lager:info([{route_id, RouteID}], "syncing new route"), - hpr_route_storage:insert(Route), - refresh_devaddrs(RouteID), - refresh_euis(RouteID), - refresh_skfs(RouteID) - end, - do_sync_all_routes(Routes, remove_route(Route, ExistingRoutes)). - -remove_route(Target, RouteList) -> - ID = hpr_route:id(Target), - lists:filter(fun(R) -> hpr_route:id(R) =/= ID end, RouteList). - -handle_call(get_all_routes, _From, State) -> - Routes = do_get_all_routes(), - {reply, {ok, Routes}, State}; -handle_call(sync_routes, _From, State) -> - Routes = do_get_all_routes(), - ExistingRoutes = hpr_route_storage:all_routes(), - do_sync_all_routes(Routes, ExistingRoutes), - {reply, ok, State}; handle_call({refresh_route, RouteID}, _From, State) -> DevaddrResponse = refresh_devaddrs(RouteID), EUIResponse = refresh_euis(RouteID), 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 a0951341..8e248279 100644 --- a/test/hpr_route_stream_worker_SUITE.erl +++ b/test/hpr_route_stream_worker_SUITE.erl @@ -17,9 +17,7 @@ reset_stream_test/1, reset_channel_test/1, app_restart_rehydrate_test/1, - route_remove_delete_skf_dets_test/1, - sync_new_route_test/1, - sync_remove_route_test/1 + route_remove_delete_skf_dets_test/1 ]). %%-------------------------------------------------------------------- @@ -40,9 +38,7 @@ all() -> reset_stream_test, reset_channel_test, app_restart_rehydrate_test, - route_remove_delete_skf_dets_test, - sync_new_route_test, - sync_remove_route_test + route_remove_delete_skf_dets_test ]. %%-------------------------------------------------------------------- @@ -546,108 +542,6 @@ main_test(_Config) -> 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. - RouteID = "7d502f32-4d58-4746-965e-001", - Route = hpr_route:test_new(#{ - id => RouteID, - net_id => 0, - oui => 1, - server => #{ - host => "localhost", - port => 8080, - protocol => {packet_router, #{}} - }, - max_copies => 10 - }), - EUIPair = hpr_eui_pair:test_new(#{route_id => RouteID, app_eui => 1, dev_eui => 0}), - DevAddrRange = hpr_devaddr_range:test_new(#{ - route_id => RouteID, start_addr => 16#00000001, end_addr => 16#0000000A - }), - DevAddr = 16#00000001, - SessionKey = hpr_utils:bin_to_hex_string(crypto:strong_rand_bytes(16)), - SessionKeyFilter = hpr_skf:new(#{ - route_id => RouteID, devaddr => DevAddr, session_key => SessionKey, max_copies => 1 - }), - 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]), - - ok = hpr_route_stream_worker:sync_routes(), - {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_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 - ok = hpr_route_stream_worker:sync_routes(), - ?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. - refresh_route_test(_Config) -> %% Create route and send them from server Route1ID = "7d502f32-4d58-4746-965e-001", @@ -831,7 +725,9 @@ test_data(RouteID) -> route_id => RouteID, route => Route1, eui_pair => EUIPair1, + devaddr => DevAddr1, devaddr_range => DevAddrRange1, + session_key => SessionKey1, skf => SessionKeyFilter1 }. From e562e05b4e0dffd152f639420eb6b2dda470763b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 2 Jul 2024 14:11:40 -0700 Subject: [PATCH 4/4] add modules for org list request and response --- src/cli/hpr_cli_config.erl | 7 +++-- src/grpc/iot_config/hpr_org.erl | 16 ---------- 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 | 18 +++++++++++ 5 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 src/grpc/iot_config/hpr_org_list_req.erl create mode 100644 src/grpc/iot_config/hpr_org_list_res.erl diff --git a/src/cli/hpr_cli_config.erl b/src/cli/hpr_cli_config.erl index 438bb58f..6b1708ad 100644 --- a/src/cli/hpr_cli_config.erl +++ b/src/cli/hpr_cli_config.erl @@ -429,7 +429,8 @@ config_route_deactivate(_, _, _) -> config_route_remove(["config", "route", "remove", RouteID], [], []) -> case hpr_route_storage:lookup(RouteID) of - {ok, Route} -> + {ok, RouteETS} -> + Route = hpr_route_ets:route(RouteETS), hpr_route_storage:delete(Route), c_text("Deleted Route: ~p", [RouteID]); {error, not_found} -> @@ -799,13 +800,13 @@ format_skf({{SKF, DevAddr}, 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(), + hpr_org_list_req:new(), #{channel => ?IOT_CONFIG_CHANNEL} ), lists:flatmap( fun(OUI) -> get_api_routes_for_oui(OUI) end, - hpr_org:list_res_ouis(OrgList) + hpr_org_list_res:org_ouis(OrgList) ). -spec get_api_routes_for_oui(OUI :: non_neg_integer()) -> list(hpr_route:route()). diff --git a/src/grpc/iot_config/hpr_org.erl b/src/grpc/iot_config/hpr_org.erl index 3f3a7c2b..abe1b078 100644 --- a/src/grpc/iot_config/hpr_org.erl +++ b/src/grpc/iot_config/hpr_org.erl @@ -2,11 +2,6 @@ -include("../autogen/iot_config_pb.hrl"). --export([ - list_req/0, - list_res_ouis/1 -]). - -export([ oui/1, owner/1, @@ -43,17 +38,6 @@ delegate_keys(Org) -> locked(Org) -> Org#iot_config_org_v1_pb.locked. -%% ------------------------------------------------------------------ -%% Org Service -%% ------------------------------------------------------------------ --spec list_req() -> #iot_config_org_list_req_v1_pb{}. -list_req() -> - #iot_config_org_list_req_v1_pb{}. - --spec list_res_ouis(#iot_config_org_list_res_v1_pb{}) -> [non_neg_integer()]. -list_res_ouis(#iot_config_org_list_res_v1_pb{orgs = Orgs}) -> - lists:map(fun(Org) -> ?MODULE:oui(Org) end, Orgs). - %% ------------------------------------------------------------------ %% Tests Functions %% ------------------------------------------------------------------ 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 index 544ab832..d52339a9 100644 --- a/src/grpc/iot_config/hpr_route_list_req.erl +++ b/src/grpc/iot_config/hpr_route_list_req.erl @@ -4,6 +4,12 @@ -export([ new/2, + timestamp/1, + signer/1, + signature/1 +]). + +-export([ sign/2, verify/1 ]). @@ -18,6 +24,18 @@ new(Signer, Oui) -> 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(