src/grisp_connect_api.erl

%% @doc Library module containing the jsonrpc API logic
-module(grisp_connect_api).

-export([handle_msg/1]).

-include_lib("kernel/include/logger.hrl").

%--- Macros --------------------------------------------------------------------
-define(method_get, get).
-define(method_post, post).


%--- API -----------------------------------------------------------------------

% @doc Handles requests and notifications from grisp.io.
-spec handle_msg(Msg) ->
    ok | {reply, Result :: term(), ReqRef :: binary() | integer()}
    | {error, Code :: integer() | atom(), Message :: binary() | undefined, ErData :: term(), ReqRef :: binary() | integer()}
  when Msg :: {request, Method :: jarl:method(), Params :: map() | list(), ReqRef :: binary() | integer()}
            | {notification, jarl:method(), Params :: map() | list()}.
handle_msg({notification, M, Params}) ->
    handle_notification(M, Params);
handle_msg({request, M, Params, ID}) ->
    try
        handle_request(M, Params, ID)
    catch
        _:R ->
            ?LOG_ERROR("Error while processing request ~s: ~p",
                       [jarl_utils:format_method(M), R]),
            ReasonBinary = iolist_to_binary(io_lib:format("~p", [R])),
            {error, internal_error, ReasonBinary, undefined, ID}
    end.


%--- Internal Funcitons --------------------------------------------------------

maybe_put(Map, []) -> Map;
maybe_put(Map, [{Key, Fun, Filter} | Rest]) ->
    maybe_put(maybe_put(Map, Key, Fun, Filter), Rest).

maybe_put(Map, Key, Fun, Filter) ->
    try Fun() of
        V when is_map(V) -> Map#{Key => maps:with(Filter, V)};
        _ -> Map
    catch
      _:_ -> Map
    end.

handle_notification([log, sync], Params) ->
    grisp_connect_log:sync(Params);
handle_notification(Method, Params) ->
    ?LOG_ERROR("Received unexpected notification ~p: ~p", [Method, Params]),
    ok.

handle_request([?method_get], #{type := <<"system_info">>} = _Params, ID) ->
    ClusterInfo = grisp_connect_cluster:system_info(),
    UpdateInfo = grisp_connect_updater:system_info(),
    Info = maybe_put(maps:merge(ClusterInfo, UpdateInfo), [
        {software, fun grisp_info:software/0,
         [id, relname, relvsn, profiles, toolchain_rev, rtems_ver, otp_ver]},
        {hardware, fun grisp_info:hardware/0,
         [platform, version, serial, batch]}
    ]),
    {reply, Info, ID};
handle_request([?method_post], #{type := <<"start_update">>} = Params, ID) ->
    try
        URL = maps:get(url, Params),
        case grisp_connect_updater:start_update(URL) of
            {error, grisp_updater_unavailable} ->
                {error, grisp_updater_unavailable, undefined, undefined, ID};
            {error, already_updating} ->
                {error, already_updating, undefined, undefined, ID};
            {error, boot_system_not_validated} ->
                {error, boot_system_not_validated, undefined, undefined, ID};
            {error, Reason} ->
                ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])),
                {error, internal_error, ReasonBinary, undefined, ID};
            ok ->
                {reply, ok, ID}
        end
    catch
        error:{badkey, _} ->
            {error, internal_error, <<"Invalid params">>, undefined, ID}
    end;
handle_request([?method_post], #{type := <<"validate">>}, ID) ->
    case grisp_connect_updater:validate() of
        {error, grisp_updater_unavailable} ->
            {error, grisp_updater_unavailable, undefined, undefined, ID};
        {error, {validate_from_unbooted, PartitionIndex}} ->
            {error, validate_from_unbooted, undefined, PartitionIndex, ID};
        {error, Reason} ->
            ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])),
            {error, internal_error, ReasonBinary, undefined, ID};
        ok ->
            {reply, ok, ID}
    end;
handle_request([?method_post], #{type := <<"reboot">>}, ID) ->
    grisp_connect_client:reboot(),
    {reply, ok, ID};
handle_request([?method_post], #{type := <<"cancel">>}, ID) ->
    case grisp_connect_updater:cancel() of
        {error, grisp_updater_unavailable} ->
            {error, grisp_updater_unavailable, undefined, undefined, ID};
        ok ->
            {reply, ok, ID}
    end;
handle_request([log, get], Params, ID) ->
    {reply, grisp_connect_log:get(Params), ID};
handle_request([cluster, join], Params, ID) ->
    #{cookie := CookieBin, ca := CAPemBin, nodename := NodeNameBin,
        fingerprint := FingerprintB64, hostname := HostnameBin,
        address := AddressBin} = Params,
    Node = binary_to_atom(NodeNameBin),
    {ok, Address} = inet:parse_ipv4_address(binary_to_list(AddressBin)),
    Opts = #{
        hostname => HostnameBin,
        address => Address,
        cookie => binary_to_atom(CookieBin),
        ca => CAPemBin,
        fingerprint => base64:decode(FingerprintB64),
        monitor => maps:get(monitor, Params, false)
    },
    case grisp_connect_cluster:join(Node, Opts) of
        error -> {reply, false, ID};
        IsConnected -> {reply, IsConnected, ID}
    end;
handle_request([cluster, leave], Params, ID) ->
    #{nodename := NodeNameBin} = Params,
    Node = binary_to_atom(NodeNameBin),
    {reply, grisp_connect_cluster:leave(Node), ID};
handle_request([cluster, list], #{}, ID) ->
    {reply, grisp_connect_cluster:list(), ID};
handle_request(Method, Params, ID) ->
    ?LOG_ERROR("Received unexpected request ~p: ~p", [Method, Params]),
    {error, method_not_found, undefined, undefined, ID}.