dexter has uploaded this change for review. ( https://gerrit.osmocom.org/c/onomondo-eim/+/42946?usp=email )
Change subject: mnesia_db: split functionality into dedicated modules ......................................................................
mnesia_db: split functionality into dedicated modules
The mnesia_db module is responsible to handle three different tables with different scope. Let's split the functionality into separate modules to make the code easier to maintain.
Change-Id: Ifc649337ab7ce7eb9dc602a1439293130f002f41 Related: SYS#8100 --- A include/mnesia_db_euicc.hrl A include/mnesia_db_rest.hrl A include/mnesia_db_work.hrl M src/crypto_utils.erl M src/esipa_asn1_handler.erl M src/esipa_asn1_handler_utils.erl M src/esipa_asn1_http_handler.erl M src/esipa_rest_utils.erl M src/mnesia_db.erl A src/mnesia_db_euicc.erl A src/mnesia_db_rest.erl A src/mnesia_db_work.erl M src/rest_handler.erl 13 files changed, 908 insertions(+), 854 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/onomondo-eim refs/changes/46/42946/1
diff --git a/include/mnesia_db_euicc.hrl b/include/mnesia_db_euicc.hrl new file mode 100644 index 0000000..57578e7 --- /dev/null +++ b/include/mnesia_db_euicc.hrl @@ -0,0 +1,10 @@ +% In this table record we save basic meta information about the eUICCs that we manage with this eIM. The information +% stored is primarily needed to authenticate eUICC Package Results. +-record(euicc, { + eidValue :: binary(), % EID of the related eUICC + counterValue :: integer(), % Counter Value (see also SGP.32, section 2.11.1.1.1) + consumerEuicc :: boolean(), % flag to mark an eUICC as consumer type (IoT eUICC emulation in onomondo-ipa) + associationToken :: integer(), % Association Token (see also SGP.32, section 2.11.1.1.1) + signPubKey :: binary(), % Pubkey to authenticate eUICC Package Results (see also SGP.32, section 2.11.2.1) + signAlgo :: binary() % Algorithem to authenticate eUICC Package Results (prime256v1 or brainpoolP256r1) +}). diff --git a/include/mnesia_db_rest.hrl b/include/mnesia_db_rest.hrl new file mode 100644 index 0000000..66c6d94 --- /dev/null +++ b/include/mnesia_db_rest.hrl @@ -0,0 +1,14 @@ +% In this table record we hold the state of a rest resource. A rest resource is created by the requestor and contains +% an order, which is processed at the next possible opportunity. The requestor can use the resource to monitor the +% status of the order he has requested. When processing is done, it is the responsibility f the requestor to request +% the deletion of the rest resource. +-record(rest, { + resourceId :: binary(), % identifier of the related REST resource + facility :: atom(), % an atom describing the facility ("download", "psmp", "eco", "edr" or "euicc") + eidValue :: binary(), % EID of the related eUICC + order :: tuple(), % order parameters received on creation of the related REST resource + status :: atom(), % an atom describing the global processing state of the order ("new", "work", or "done") + timestamp :: integer(), % timestamp of the last state change + outcome :: any(), % outcome of the requested order + debuginfo :: any() % elang term containing debug information (presented as binary string to the requestor) +}). diff --git a/include/mnesia_db_work.hrl b/include/mnesia_db_work.hrl new file mode 100644 index 0000000..b864141 --- /dev/null +++ b/include/mnesia_db_work.hrl @@ -0,0 +1,13 @@ +% In this table record we hold the state (work item) of a transaction that is currently processed (worked on). A work +% item is created when a requiest from the IPAd positively maps to a pending order in the REST table +% (see mnesia_db_rest). A work item can be understood as the dynamic state of the ongoing connection between eIM and +% IPAd (and SM-DP+). When the transaction is done and the work is finished, the related work item is deleted and the +% outcome is recorded at the related REST resource. +-record(work, { + pid :: pid(), % PID of the sub-process (cowbow) on which the work item is processed. + resourceId :: binary(), % identifier of the related REST resource + transactionId :: binary(), % transaction ID (either eimTransactionId or transactionId, see also SGP.32 and SGP.22) + eidValue :: binary(), % EID of the related eUICC + order :: tuple(), % order parameters received on creation of the related REST resource + state :: any() % intermediate results and states generated while processing the requested order +}). diff --git a/src/crypto_utils.erl b/src/crypto_utils.erl index aca40e9..63abc59 100644 --- a/src/crypto_utils.erl +++ b/src/crypto_utils.erl @@ -40,7 +40,7 @@
sign_euiccPackageSigned(EuiccPackageSigned, EidValue) -> % Read the AssociationToken - {ok, AssociationToken} = mnesia_db:euicc_param_get(EidValue, associationToken), + {ok, AssociationToken} = mnesia_db_euicc:param_get(EidValue, associationToken),
%Format message to be signed {ok, EuiccPackageSignedEnc} = 'SGP32Definitions':encode( @@ -64,10 +64,10 @@ verify_signature(Message, Signature, EidValue) -> DERSignature = plain_to_der(Signature),
- {ok, SubjectPublicKeyHex} = mnesia_db:euicc_param_get(EidValue, signPubKey), + {ok, SubjectPublicKeyHex} = mnesia_db_euicc:param_get(EidValue, signPubKey), SubjectPublicKey = utils:hex_to_binary(SubjectPublicKeyHex),
- {ok, SignAlgo} = mnesia_db:euicc_param_get(EidValue, signAlgo), + {ok, SignAlgo} = mnesia_db_euicc:param_get(EidValue, signAlgo), NamedCurve = case SignAlgo of <<"prime256v1">> -> @@ -108,11 +108,11 @@ end.
verify_euiccPackageResultSigned(EuiccPackageResult, EidValue) -> - {ok, ConsumerEuicc} = mnesia_db:euicc_param_get(EidValue, consumerEuicc), + {ok, ConsumerEuicc} = mnesia_db_euicc:param_get(EidValue, consumerEuicc), case ConsumerEuicc of false -> % Read the AssociationToken - {ok, AssociationToken} = mnesia_db:euicc_param_get(EidValue, associationToken), + {ok, AssociationToken} = mnesia_db_euicc:param_get(EidValue, associationToken),
case EuiccPackageResult of {euiccPackageResultSigned, EuiccPackageResultSigned} -> @@ -275,15 +275,15 @@ _ -> <<"unknown">> end, - ok = mnesia_db:euicc_param_set(EidValue, signPubKey, utils:binary_to_hex(SignPubKey)), - ok = mnesia_db:euicc_param_set(EidValue, signAlgo, SignAlgo), + ok = mnesia_db_euicc:param_set(EidValue, signPubKey, utils:binary_to_hex(SignPubKey)), + ok = mnesia_db_euicc:param_set(EidValue, signAlgo, SignAlgo), ok; _ -> error end.
store_euicc_pubkey_from_authenticateResponseOk(AuthRespOk, EidValue) -> - case mnesia_db:euicc_param_get(EidValue, signPubKey) of + case mnesia_db_euicc:param_get(EidValue, signPubKey) of {ok, <<>>} -> % There is no public key stored yet for this eUICC, use the public % key provided in the eUICC certificate @@ -296,7 +296,7 @@ end.
store_euicc_pubkey_from_ipaEuiccDataResponse(IpaEuiccDataResponse, EidValue) -> - case mnesia_db:euicc_param_get(EidValue, signPubKey) of + case mnesia_db_euicc:param_get(EidValue, signPubKey) of {ok, <<>>} -> % There is no public key stored yet for this eUICC, use the public % key provided in the eUICC certificate diff --git a/src/esipa_asn1_handler.erl b/src/esipa_asn1_handler.erl index 111ef17..82a4290 100644 --- a/src/esipa_asn1_handler.erl +++ b/src/esipa_asn1_handler.erl @@ -19,10 +19,10 @@
%GSMA SGP.32, section 6.3.2.1 handle_asn1(Pid, {initiateAuthenticationRequestEsipa, EsipaReq}) -> - {_, _, WorkState} = mnesia_db:work_pickup(Pid, none), + {_, _, WorkState} = mnesia_db_work:pickup(Pid, none), BaseUrl = maps:get(smdpAddress, EsipaReq), NewWorkState = WorkState#{smdpAddress => BaseUrl}, - mnesia_db:work_update(Pid, NewWorkState), + mnesia_db_work:update(Pid, NewWorkState),
% euiccInfo1 is also an optional field in initiateAuthenticationRequestEsipa and a mandatory field in % initiateAuthenticationRequest. However the field is only missing in case the IPA capability minimizeEsipaBytes is @@ -39,7 +39,7 @@ case Es9Resp of {initiateAuthenticationOk, InitAuthOk} -> TransactionId = maps:get(transactionId, InitAuthOk), - mnesia_db:work_bind(Pid, TransactionId), + mnesia_db_work:bind(Pid, TransactionId), % TODO: matchingId and ctxParams1 are not defined in the ES9+ InitiateAuthenticationResponse message. % However in ESipa those fields are optional fields and either one of it should be populated in case an % AC is used (which we do). This means we should populate those fields. The matchingId can be extracted @@ -57,7 +57,7 @@ }, {initiateAuthenticationOkEsipa, InitAuthOkEsipa}; {initiateAuthenticationError, InitAuthErr} -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, initiateAuthenticationError}]}], EsipaReq @@ -68,7 +68,7 @@ %GSMA SGP.32, section 6.3.2.2 handle_asn1(Pid, {authenticateClientRequestEsipa, EsipaReq}) -> TransactionId = maps:get(transactionId, EsipaReq), - {EidValue, _, WorkState} = mnesia_db:work_pickup(Pid, TransactionId), + {EidValue, _, WorkState} = mnesia_db_work:pickup(Pid, TransactionId), BaseUrl = maps:get(smdpAddress, WorkState),
% setup ES9+ request message @@ -83,7 +83,7 @@ authenticateServerResponse => {authenticateResponseOk, AuthRespOk} }}; {authenticateResponseError, AuthRespErr} -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, authenticateResponseError}]}], EsipaReq @@ -106,7 +106,7 @@ {authenticateClientOk, AuthClntRespEs9} -> {authenticateClientOkDPEsipa, AuthClntRespEs9}; {authenticateClientError, AuthClntErr} -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, authenticateClientError}]}], EsipaReq @@ -117,7 +117,7 @@ %GSMA SGP.32, section 6.3.2.3 handle_asn1(Pid, {getBoundProfilePackageRequestEsipa, EsipaReq}) -> TransactionId = maps:get(transactionId, EsipaReq), - {_, _, WorkState} = mnesia_db:work_pickup(Pid, TransactionId), + {_, _, WorkState} = mnesia_db_work:pickup(Pid, TransactionId), BaseUrl = maps:get(smdpAddress, WorkState),
% setup ES9+ request message @@ -130,7 +130,7 @@ prepareDownloadResponse => {downloadResponseOk, DwnldRespOk} }}; {downloadResponseError, DwnldRespErr} -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, downloadResponseError}]}], EsipaReq @@ -155,7 +155,7 @@ % however, this eIM does not support the IPA capability minimizeEsipaBytes) {getBoundProfilePackageOkEsipa, GetBndPrflePkgOk}; {getBoundProfilePackageError, GetBndPrflePkgErr} -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, getBoundProfilePackageError}]}], EsipaReq @@ -166,7 +166,7 @@ %GSMA SGP.32, section 6.3.2.5 handle_asn1(Pid, {cancelSessionRequestEsipa, EsipaReq}) -> TransactionId = maps:get(transactionId, EsipaReq), - {_, _, WorkState} = mnesia_db:work_pickup(Pid, TransactionId), + {_, _, WorkState} = mnesia_db_work:pickup(Pid, TransactionId), BaseUrl = maps:get(smdpAddress, WorkState),
% setup ES9+ request message @@ -179,7 +179,7 @@ cancelSessionResponse => {cancelSessionResponseOk, CancelSessionRespOk} }}; {cancelSessionResponseError, CancelSessionRespErr} -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, cancelSessionResponseError}]}], EsipaReq @@ -197,7 +197,7 @@ {cancelSessionResponseEs9, Es9Resp} = es9p_client:request_json(Es9Req, BaseUrl),
Outcome = esipa_rest_utils:cancelSessionResponse_to_outcome(CancelSessionResp), - ok = mnesia_db:work_finish(Pid, Outcome, EsipaReq), + ok = mnesia_db_work:finish(Pid, Outcome, EsipaReq),
% setup ESipa response message % CancelSessionResponseEsipa and CancelSessionResponseEs9 share the exact same definition, so we may convert @@ -226,7 +226,7 @@ % Notification Receivers (see also GSMA SGP.32, section 3.7) procedure. By then the context in the % eIM may be long gone. The eIM will be unable to match the ProfileInstallationResult to any % context but it will foward it to the SMDP+ anyway. - case mnesia_db:work_bind(Pid, TransactionId) of + case mnesia_db_work:bind(Pid, TransactionId) of ok -> % A work item exists, foward the ProfileInstallationResult and make its contents % available to the REST API user @@ -235,9 +235,9 @@ Outcome = esipa_rest_utils:profileInstallationResult_to_outcome( PrfleInstRslt ), - ok = mnesia_db:work_finish(Pid, Outcome, EsipaReq); + ok = mnesia_db_work:finish(Pid, Outcome, EsipaReq); _ -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, handleNotificationError}]}], EsipaReq @@ -280,20 +280,20 @@ % update. Get the notifyStateChange flag like so: NotifStateChg = maps:is_key(notifyStateChange, EsipaReq).
EidValue = maps:get(eidValue, EsipaReq), - Work = mnesia_db:work_fetch(utils:binary_to_hex(EidValue), Pid), + Work = mnesia_db_work:fetch(utils:binary_to_hex(EidValue), Pid), EsipaResp = case Work of {download, Order} -> % The first time we see a TransactionId is in the SMDP+ response to the % initiateAuthenticationRequest {[{<<"download">>, {[{<<"activationCode">>, ActivationCode}]}}]} = Order, - mnesia_db:work_update(Pid, #{}), + mnesia_db_work:update(Pid, #{}), {profileDownloadTriggerRequest, #{ profileDownloadData => {activationCode, ActivationCode} }}; {psmo, Order} -> TransactionIdPsmo = rand:bytes(16), - mnesia_db:work_bind(Pid, TransactionIdPsmo), + mnesia_db_work:bind(Pid, TransactionIdPsmo), EuiccPackageSigned = esipa_rest_utils:psmo_order_to_euiccPackageSigned( Order, EidValue, @@ -301,7 +301,7 @@ ), case EuiccPackageSigned of error -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, badPsmo}]}], EsipaReq @@ -319,7 +319,7 @@ end; {eco, Order} -> TransactionIdEco = rand:bytes(16), - mnesia_db:work_bind(Pid, TransactionIdEco), + mnesia_db_work:bind(Pid, TransactionIdEco), EuiccPackageSigned = esipa_rest_utils:eco_order_to_euiccPackageSigned( Order, EidValue, @@ -327,7 +327,7 @@ ), case EuiccPackageSigned of error -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, badEco}]}], EsipaReq @@ -347,7 +347,7 @@ IpaEuiccDataRequest = esipa_rest_utils:edr_order_to_ipaEuiccDataRequest(Order), case IpaEuiccDataRequest of error -> - ok = mnesia_db:work_finish( + ok = mnesia_db_work:finish( Pid, [{[{procedureError, badEdr}]}], EsipaReq @@ -359,7 +359,7 @@ none -> {eimPackageError, noEimPackageAvailable}; _ -> - ok = mnesia_db:work_finish(Pid, [{[{procedureError, badOrder}]}], EsipaReq), + ok = mnesia_db_work:finish(Pid, [{[{procedureError, badOrder}]}], EsipaReq), {eimPackageError, undefinedError} end, {getEimPackageResponse, EsipaResp}; @@ -382,12 +382,12 @@ handle_asn1_notificationList(Pid, NotificationList); {ipaEuiccDataResponse, IpaEuiccDataResponse} -> % drive-by store the eUICC public key so that we can use it later to sign PSMOs or eCOs - {EidValue, _, _} = mnesia_db:work_pickup(Pid, none), + {EidValue, _, _} = mnesia_db_work:pickup(Pid, none), crypto_utils:store_euicc_pubkey_from_ipaEuiccDataResponse( IpaEuiccDataResponse, EidValue ), Outcome = esipa_rest_utils:ipaEuiccDataResponse_to_outcome(IpaEuiccDataResponse), - ok = mnesia_db:work_finish(Pid, Outcome, EsipaReq); + ok = mnesia_db_work:finish(Pid, Outcome, EsipaReq); {profileDownloadTriggerResult, _} -> % The profileDownloadTriggerResult is sent by the IPAd in case a profile was downloaded directly from an % RSP server, bypassing the eIM (see also SGP.32, section 3.2.3.1). This is a feature that this eIM does @@ -398,12 +398,12 @@ eimPackageResultErrorCode, EimPackageResultResponseError ), Outcome = [{[{eimPackageError, EimPackageResultErrorCode}]}], - ok = mnesia_db:work_finish(Pid, Outcome, EsipaReq) + ok = mnesia_db_work:finish(Pid, Outcome, EsipaReq) end, {provideEimPackageResultResponse, {emptyResponse, #{}}}; %Unsupported request handle_asn1(Pid, Request) -> - mnesia_db:work_finish(Pid, [{[{procedureError, abortedOrder}]}], unsupported), + mnesia_db_work:finish(Pid, [{[{procedureError, abortedOrder}]}], unsupported), logger:info( "Handling of IPAd request failed, the request type is unsupported,~nRequest=~p,~nPid=~p~n", [Request, Pid] diff --git a/src/esipa_asn1_handler_utils.erl b/src/esipa_asn1_handler_utils.erl index c297638..ac7d6f0 100644 --- a/src/esipa_asn1_handler_utils.erl +++ b/src/esipa_asn1_handler_utils.erl @@ -28,16 +28,16 @@ WorkBind = fun(Map) -> case maps:is_key(eimTransactionId, Map) of true -> - mnesia_db:work_bind(Pid, maps:get(eimTransactionId, Map)); + mnesia_db_work:bind(Pid, maps:get(eimTransactionId, Map)); _ -> ok end end,
CheckCounterValue = fun(Map) -> - {EidValue, _, _} = mnesia_db:work_pickup(Pid, TransactionId), + {EidValue, _, _} = mnesia_db_work:pickup(Pid, TransactionId), CounterValueIpad = maps:get(counterValue, Map), - {ok, CounterValueEim} = mnesia_db:euicc_param_get(EidValue, counterValue), + {ok, CounterValueEim} = mnesia_db_euicc:param_get(EidValue, counterValue), case CounterValueIpad of CounterValueEim -> ok; @@ -83,18 +83,18 @@ [{[{euiccPackageErrorCode, undefinedError}]}] end,
- mnesia_db:work_finish(Pid, Outcome, EsipaReq). + mnesia_db_work:finish(Pid, Outcome, EsipaReq).
% Handle an EuiccPackageResult, this includes everything from the handling of the work items in mnesia_db, down to % signature checks and the generation of an appropriate outcome for the REST API. handle_euiccPackageResult(Pid, EuiccPackageResult, EsipaReq) -> TransactionId = transactionId_from_euiccPackageResult(EuiccPackageResult), - {EidValue, _, _} = mnesia_db:work_pickup(Pid, TransactionId), + {EidValue, _, _} = mnesia_db_work:pickup(Pid, TransactionId), case crypto_utils:verify_euiccPackageResultSigned(EuiccPackageResult, EidValue) of ok -> process_euiccPackageResult(Pid, EuiccPackageResult, EsipaReq, TransactionId); _ -> - mnesia_db:work_finish( + mnesia_db_work:finish( Pid, [{[{procedureError, euiccSignatureInvalid}]}], EsipaReq ) end. diff --git a/src/esipa_asn1_http_handler.erl b/src/esipa_asn1_http_handler.erl index ff5fcff..5f6ff9d 100644 --- a/src/esipa_asn1_http_handler.erl +++ b/src/esipa_asn1_http_handler.erl @@ -60,7 +60,7 @@ normal -> ok; _ -> - mnesia_db:work_finish( + mnesia_db_work:finish( maps:get(pid, Req0), [{[{procedureError, abortedOrder}]}], Reason ), logger:info( diff --git a/src/esipa_rest_utils.erl b/src/esipa_rest_utils.erl index d026dc0..78939ab 100644 --- a/src/esipa_rest_utils.erl +++ b/src/esipa_rest_utils.erl @@ -190,7 +190,7 @@ error; _ -> {ok, EimId} = application:get_env(onomondo_eim, eim_id), - {ok, CounterValue} = mnesia_db:euicc_counter_tick(utils:binary_to_hex(EidValue)), + {ok, CounterValue} = mnesia_db_euicc:counter_tick(utils:binary_to_hex(EidValue)), #{ eimId => list_to_binary(EimId), eidValue => EidValue, diff --git a/src/mnesia_db.erl b/src/mnesia_db.erl index 6a2b3e5..cd69e3a 100644 --- a/src/mnesia_db.erl +++ b/src/mnesia_db.erl @@ -6,71 +6,13 @@
-module(mnesia_db). -include_lib("stdlib/include/qlc.hrl"). +-include("mnesia_db_rest.hrl"). +-include("mnesia_db_work.hrl"). +-include("mnesia_db_euicc.hrl").
% Initialization, startup -export([init/0]).
-% REST functions, to be called by the REST server (from outside via cowboy) --export([rest_list/1, rest_lookup/2, rest_create/3, rest_delete/2]). - -% work functions, to be called by the eIM code (from inside) --export([work_fetch/2, work_pickup/2, work_update/2, work_bind/2, work_finish/3]). - -% euicc functions, to be called by the eIM code (from inside) --export([euicc_counter_tick/1, euicc_param_get/2, euicc_param_set/3]). - -% debugging --export([dump_rest/0, dump_work/0, dump_euicc/0]). - -% trigger recurring events (called automatically by timer from this module) --export([cleanup/0, euicc_setparam/0]). - --record(rest, { - resourceId :: binary(), - facility :: atom(), - eidValue :: binary(), - order :: binary(), - status :: atom(), - timestamp :: integer(), - outcome :: binary(), - debuginfo :: binary() -}). --record(work, { - pid :: pid(), - resourceId :: binary(), - transactionId :: binary(), - eidValue :: binary(), - order :: binary(), - state :: binary() -}). --record(euicc, { - eidValue :: binary(), - counterValue :: integer(), - consumerEuicc :: boolean(), - associationToken :: integer(), - signPubKey :: binary(), - signAlgo :: binary() -}). - -% Caution: The status (atom) must be either "new", "work", or "done" - -% helper function (to be called from a transaction) to set the status of an item in the rest table. -trans_rest_set_status(ResourceId, Status, Outcome, Debuginfo) -> - Q = qlc:q([X || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]), - Rows = qlc:e(Q), - Timestamp = os:system_time(seconds), - case Rows of - [Row | []] -> - mnesia:write(Row#rest{ - status = Status, timestamp = Timestamp, outcome = Outcome, debuginfo = Debuginfo - }), - ok; - [] -> - error; - _ -> - error - end. - % Initialize databse scheme, tables and check the database for unfinished orders init() -> % Create an initial mnesia scheme. @@ -154,7 +96,7 @@ ResourceIds = qlc:e(Q), lists:foreach( fun(ResourceId) -> - trans_rest_set_status( + mnesia_db_rest:trans_set_status( ResourceId, done, [{[{procedureError, abortedOrder}]}], @@ -167,748 +109,7 @@ {atomic, ok} = mnesia:transaction(Trans),
% Start recurring event cycles - ok = cleanup(), - ok = euicc_setparam(), + ok = mnesia_db_euicc:timer_setparam(), + ok = mnesia_db_rest:timer_cleanup(),
ok. - -% Create REST resource (order) -rest_create(Facility, EidValue, Order) -> - ok = euicc_create_if_not_exist(EidValue), - ResourceId = uuid:uuid_to_string(uuid:get_v4_urandom()), - Timestamp = os:system_time(seconds), - Row = #rest{ - resourceId = ResourceId, - facility = Facility, - eidValue = EidValue, - order = Order, - status = new, - timestamp = Timestamp, - outcome = [], - debuginfo = none - }, - Trans = fun() -> - Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]), - Present = qlc:e(Q), - case Present of - [] -> - mnesia:write(Row); - _ -> - error - end - end, - {atomic, Result} = mnesia:transaction(Trans), - {Result, ResourceId}. - -% Lookup REST resource (order) -rest_lookup(ResourceId, Facility) -> - Trans = fun() -> - Q = qlc:q([ - { - X#rest.status, - X#rest.timestamp, - X#rest.eidValue, - X#rest.order, - X#rest.outcome, - X#rest.debuginfo - } - || X <- mnesia:table(rest), - X#rest.resourceId == ResourceId, - X#rest.facility == Facility - ]), - qlc:e(Q) - end, - {atomic, Result} = mnesia:transaction(Trans), - case Result of - [{Status, Timestamp, EidValue, Order, Outcome, Debuginfo}] -> - {Status, Timestamp, EidValue, Order, Outcome, Debuginfo}; - [] -> - none; - _ -> - error - end. - -% Delete REST resource (order) -rest_delete(ResourceId, Facility) -> - Trans = fun() -> - QRest = qlc:q([ - X#rest.resourceId - || X <- mnesia:table(rest), - X#rest.resourceId == ResourceId, - X#rest.facility == Facility - ]), - RestPresent = qlc:e(QRest), - case RestPresent of - [] -> - none; - _ -> - OidRest = {rest, ResourceId}, - ok = mnesia:delete(OidRest), - - % There may be an orphaned work item now, which we must also remove. This will also kill - % the order in case it is currently in progress. - QWork = qlc:q([ - X#work.pid - || X <- mnesia:table(work), X#work.resourceId == ResourceId - ]), - WorkPresent = qlc:e(QWork), - case WorkPresent of - [] -> - ok; - [Pid] -> - OidWork = {work, Pid}, - mnesia:delete(OidWork); - _ -> - error - end - end - end, - {atomic, Result} = mnesia:transaction(Trans), - Result. - -% List the resource identifiers of currently present REST resources -rest_list(Facility) -> - Trans = fun() -> - Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.facility == Facility]), - qlc:e(Q) - end, - {atomic, Result} = mnesia:transaction(Trans), - Result. - -% Start working on an order by creating a an entry in the work table and marking it's status as "work". After calling -% this it is the callers responsibility to handle the work item and call rest_finish_order when the work is done. -work_fetch(EidValue, Pid) -> - % One process can only work on one work item at a time. The API user must call work_finish before the next work - % item can be processed. If there is already a pending work item under the given PID, forcefully finish this work - % item. - TransPidExists = fun() -> - Q = qlc:q([X || X <- mnesia:table(work), X#work.pid == Pid]), - WorkPresent = qlc:e(Q), - case WorkPresent of - [] -> - false; - _ -> - true - end - end, - - {atomic, PidExists} = mnesia:transaction(TransPidExists), - case PidExists of - true -> - work_finish(Pid, [{[{procedureError, stuckOrder}]}], none); - false -> - ok - end, - - % Read the next pending REST resource from the rest table and create a related work item. The work item is then - % in progress. - Trans = fun() -> - Q = qlc:q([ - X - || X <- mnesia:table(rest), - X#rest.eidValue == EidValue, - X#rest.status == new, - X#rest.facility =/= euicc - ]), - Rows = qlc:e(Q), - case Rows of - [Row | _] -> - % Create an entry in the work table - WorkRow = #work{ - pid = Pid, - resourceId = Row#rest.resourceId, - transactionId = none, - eidValue = Row#rest.eidValue, - order = Row#rest.order, - state = none - }, - case mnesia:write(WorkRow) of - ok -> - % We are now working on this order - ok = trans_rest_set_status(Row#rest.resourceId, work, [], none), - Row; - _ -> - error - end; - [] -> - none; - _ -> - error - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - case Result of - {rest, _, Facility, _, Order, _, _, _, _} -> - logger:info( - "Work: fetching new work item,~nEidValue=~p, Pid=~p, Facility=~p,~nOrder=~p~n", - [EidValue, Pid, Facility, Order] - ), - {Facility, Order}; - none -> - logger:info("Work: no work item in database,~nEidValue=~p, Pid=~p~n", [EidValue, Pid]), - none; - _ -> - logger:error("Work: cannot fetch work item, database error,~nEidValue=~p, Pid=~p~n", [ - EidValue, Pid - ]), - error - end. - -% Bind a work item to a TransactionId. The transactionId has to be a unique identifier that can be used as a secondary -% key to find a work item in the databse. The binding works in two directions. The work item is first searched by its -% pid, when found, the transactionId is updated. In case the pid has become invalid, then the work item is searched -% again by the transactionId and when found, the pid is updated. This function can be called any time after work_fetch -% was called before. It can also be called multiple times. -work_bind(Pid, TransactionId) -> - % Transaction to update the TransactionId. This is the normal case. A work item starts without having a - % TransactionId assigned. As soon as a (new) TransactionId becomes known, it is updated using this Transaction. - TransUpdateTrnsId = fun() -> - Q = qlc:q([X || X <- mnesia:table(work), X#work.pid == Pid]), - Rows = qlc:e(Q), - case Rows of - [Row | _] -> - mnesia:write(Row#work{transactionId = TransactionId}); - [] -> - none; - _ -> - error - end - end, - - % Transaction to update the PID. This is a corner case that comes into play in case the PID is lost (the - % process/connection handling this work item has died). We then try to find the work item by the TransactionId - % and update its PID. - TransUpdatePid = fun() -> - Q = qlc:q([X || X <- mnesia:table(work), X#work.transactionId == TransactionId]), - Rows = qlc:e(Q), - case Rows of - [Row | _] -> - mnesia:write(Row#work{pid = Pid}); - [] -> - none; - _ -> - error - end - end, - - {atomic, Result} = mnesia:transaction(TransUpdateTrnsId), - case Result of - ok -> - logger:info("Work: bound work item to transactionId,~nPid=~p, TransactionId=~p~n", [ - Pid, TransactionId - ]), - ok; - none -> - {atomic, UpdatePidResult} = mnesia:transaction(TransUpdatePid), - case UpdatePidResult of - ok -> - logger:info("Work: bound work item to PID,~nPid=~p, TransactionId=~p~n", [ - Pid, TransactionId - ]), - ok; - none -> - logger:error( - "Work: cannot bind work item, transactionId nor PID found,~nPid=~p, TransactionId=~p~n", - [Pid, TransactionId] - ), - error; - _ -> - logger:error( - "Work: cannot bind work item, database error,~nPid=~p, TransactionId=~p, TransUpdatePid~n", - [Pid, TransactionId] - ), - error - end; - _ -> - logger:error( - "Work: cannot bind work item, database error,~nPid=~p, TransactionId=~p, TransUpdateTrnsId~n", - [Pid, TransactionId] - ), - error - end. - -% Pickup a work item that is in progress. This function can be called any time after work_fetch was called -% before. It can also be called multiple times. -work_pickup(Pid, TransactionId) -> - % In case a TransactionId is provied, bind the PID to this TransactionId, - WorkBound = - case TransactionId of - none -> - ok; - _ -> - work_bind(Pid, TransactionId) - end, - - % Lookup the work state by the given PID - case WorkBound of - ok -> - Trans = fun() -> - Q = qlc:q([ - {X#work.eidValue, X#work.order, X#work.state} - || X <- mnesia:table(work), X#work.pid == Pid - ]), - qlc:e(Q) - end, - {atomic, Result} = mnesia:transaction(Trans), - case Result of - [{EidValue, Order, State} | _] -> - {EidValue, Order, State}; - [] -> - logger:error( - "Work: no work item found under specified Pid, already finished?, not fetched?,~nPid=~p~n", - [Pid] - ), - none; - _ -> - logger:error("Work: cannot pick up work item, database error,~nPid=~p~n", [Pid]), - error - end; - _ -> - WorkBound - end. - -% Update a work item that is in progress. This fuction updates the state (any user defined term) of the work item. -% This function can be called any time after work_fetch was called before. It can also be called multiple times. -work_update(Pid, State) -> - Trans = fun() -> - Q = qlc:q([X || X <- mnesia:table(work), X#work.pid == Pid]), - Rows = qlc:e(Q), - case Rows of - [Row | _] -> - mnesia:write(Row#work{state = State}); - [] -> - error; - _ -> - error - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - case Result of - ok -> - logger:info("Work: updating work item,~nPid=~p, State=~p~n", [Pid, State]), - ok; - _ -> - logger:error("Work: cannot update, database error,~nPid=~p, State=~p~n", [Pid, State]), - error - end. - -% Finish an order that has been worked on. This removes the related entry from the work table and sets the status in -% the rest table to "done". -work_finish(Pid, Outcome, Debuginfo) -> - Trans = fun() -> - Q = qlc:q([X#work.resourceId || X <- mnesia:table(work), X#work.pid == Pid]), - Rows = qlc:e(Q), - case Rows of - [] -> - error; - [ResourceId | _] -> - Oid = {work, Pid}, - ok = mnesia:delete(Oid), - ok = trans_rest_set_status(ResourceId, done, Outcome, Debuginfo) - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - case Result of - ok -> - logger:info("Work: finishing work item,~nPid=~p, Outcome=~p~n", [Pid, Outcome]), - ok; - _ -> - logger:error( - "Work: cannot finish work item, database error,~nPid=~p, Outcome=~p~n", - [Pid, Outcome] - ), - error - end. - -trans_euicc_create_if_not_exist(EidValue) -> - {ok, CounterValue} = application:get_env(onomondo_eim, counter_value), - {ok, ConsumerEuicc} = application:get_env(onomondo_eim, consumer_euicc), - Row = #euicc{ - eidValue = EidValue, - counterValue = CounterValue, - consumerEuicc = ConsumerEuicc, - associationToken = 1, - signPubKey = <<>>, - signAlgo = <<"prime256v1">> - }, - Q = qlc:q([X#euicc.eidValue || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), - Present = qlc:e(Q), - case Present of - [] -> - mnesia:write(Row); - _ -> - present - end. - -% Create a new eUICC master data entry -euicc_create_if_not_exist(EidValue) -> - Trans = fun() -> - trans_euicc_create_if_not_exist(EidValue) - end, - {atomic, Result} = mnesia:transaction(Trans), - case Result of - ok -> - logger:info("eUICC: creating new master data entry,~neID=~p~n", [EidValue]), - ok; - present -> - ok; - _ -> - logger:error("eUICC: cannot create master data entry, database error,~neID=~p~n", [ - EidValue - ]), - error - end. - -% get an incremented counterValue (and store the incremented counterValue as the current counterValue) -euicc_counter_tick(EidValue) -> - Trans = fun() -> - Q = qlc:q([X || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), - Rows = qlc:e(Q), - case Rows of - [Row | _] -> - CounterValue = Row#euicc.counterValue + 1, - ok = mnesia:write(Row#euicc{counterValue = CounterValue}), - {ok, CounterValue}; - [] -> - error; - _ -> - error - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - case Result of - {ok, CounterValue} -> - logger:info("eUICC: incrementing counterValue,~neID=~p, counter=~p~n", [ - EidValue, CounterValue - ]), - {ok, CounterValue}; - _ -> - logger:error("eUICC: cannot increment counterValue, database error,~neID=~p~n", [ - EidValue - ]), - error - end. - -%update one specific parameter in the euicc table -trans_update_euicc_param(EidValue, Name, Value) -> - Q = qlc:q([X || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), - Rows = qlc:e(Q), - case Rows of - [Row | []] -> - case Name of - counterValue -> - mnesia:write(Row#euicc{counterValue = Value}); - consumerEuicc -> - mnesia:write(Row#euicc{consumerEuicc = Value}); - associationToken -> - mnesia:write(Row#euicc{associationToken = Value}); - signPubKey -> - mnesia:write(Row#euicc{signPubKey = Value}); - signAlgo -> - mnesia:write(Row#euicc{signAlgo = Value}); - _ -> - error - end; - [] -> - error; - _ -> - error - end. - -% Get an eUICC parameter by its name (atom) -euicc_param_get(EidValue, Name) -> - Trans = fun() -> - Q = qlc:q([X || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), - Rows = qlc:e(Q), - case Rows of - [Row | _] -> - case Name of - counterValue -> - {ok, Row#euicc.counterValue}; - consumerEuicc -> - {ok, Row#euicc.consumerEuicc}; - associationToken -> - {ok, Row#euicc.associationToken}; - signPubKey -> - {ok, Row#euicc.signPubKey}; - signAlgo -> - {ok, Row#euicc.signAlgo}; - _ -> - error - end; - [] -> - error; - _ -> - error - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - case Result of - {ok, Value} -> - logger:info("eUICC: reading eUICC parameter,~neID=~p, name=~p, value=~p~n", [ - EidValue, Name, Value - ]), - {ok, Value}; - _ -> - logger:error("eUICC: cannot read eUICC parameter,~neID=~p, name=~p~n", [EidValue, Name]), - error - end. - -% Update an eUICC parameter by its name (atom) -euicc_param_set(EidValue, Name, Value) -> - Trans = fun() -> - trans_update_euicc_param(EidValue, Name, Value) - end, - {atomic, Result} = mnesia:transaction(Trans), - case Result of - ok -> - logger:info("eUICC: writing eUICC parameter,~neID=~p, name=~p, value=~p~n", [ - EidValue, Name, Value - ]), - ok; - _ -> - logger:error("eUICC: cannot write eUICC parameter,~neID=~p, name=~p~n", [EidValue, Name]), - error - end. - -mark_stuck(Timeout) -> - % There may be corner cases where the order gets stuck because the other entity suddenly stops responding. In - % this case the order will stall. When it remains stalled for too long (minutes), we should remove all related - % items from the work table and put an appropriate outcome (error code) into the rest table. - - TimestampNow = os:system_time(seconds), - - % Remove Resource from work table and set an appropriate status in the rest table - HandleResource = fun(ResourceId) -> - % find the pid of the work item that is stuck and then delete it - Q = qlc:q([X#work.pid || X <- mnesia:table(work), X#work.resourceId == ResourceId]), - WorkPresent = qlc:e(Q), - case WorkPresent of - [Pid] -> - Oid = {work, Pid}, - ok = mnesia:delete(Oid); - _ -> - ok - end, - - % set status in the rest table - trans_rest_set_status(ResourceId, done, [{[{procedureError, stuckOrder}]}], none) - end, - - % Find all rest resources that stall in status "work" and older than the specified timeout value - Trans = fun() -> - Q = qlc:q([ - X#rest.resourceId - || X <- mnesia:table(rest), - X#rest.status == work, - TimestampNow - X#rest.timestamp > Timeout - ]), - Rows = qlc:e(Q), - case Rows of - [] -> - ok; - Rows -> - [HandleResource(Row) || Row <- Rows], - ok - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - Result. - -mark_noshow(Timeout) -> - % There may be corner cases where the order is never processed because the related IPAd/eUICC never shows up to - % fetch the related eUICC package. If we see an order staying in status "new" for too long (days, weeks), we should - % mark it as "done" and put an appropriate outcome (error code) into the rest table. - - TimestampNow = os:system_time(seconds), - - % Remove Resource from work table and set an appropriate status in the rest table - HandleResource = fun(ResourceId) -> - trans_rest_set_status(ResourceId, done, [{[{procedureError, noshowOrder}]}], none) - end, - - % Find all rest resources that stall in status "work" and older than the specified timeout value - Trans = fun() -> - Q = qlc:q([ - X#rest.resourceId - || X <- mnesia:table(rest), - X#rest.status == new, - TimestampNow - X#rest.timestamp > Timeout - ]), - Rows = qlc:e(Q), - case Rows of - [] -> - ok; - Rows -> - [HandleResource(Row) || Row <- Rows], - ok - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - Result. - -delete_expired(Timeout) -> - % There may be cases where orders stay unmaintained for too long. When an order stays in status "done" for too long - % (hours, days), than this may mean that the REST API user lost interest. In this case the related items shoud be - % removed from the rest table after a reasonable timeout. - - TimestampNow = os:system_time(seconds), - - % Remove Resource from the rest table. Since an abandonned order won't have a coresponding work item in the work - % table we do not have to worry about creating an orphaned work item. - HandleResource = fun(ResourceId) -> - Oid = {rest, ResourceId}, - ok = mnesia:delete(Oid) - end, - - % Find all rest resources that linger in the rest table for a long time and are not in the status "new" - Trans = fun() -> - Q = qlc:q([ - X#rest.resourceId - || X <- mnesia:table(rest), - X#rest.status == done, - TimestampNow - X#rest.timestamp > Timeout - ]), - Rows = qlc:e(Q), - case Rows of - [] -> - ok; - Rows -> - [HandleResource(Row) || Row <- Rows], - ok - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - Result. - -% Run a cleanup cycle the database -cleanup() -> - {ok, RestTimeoutStuck} = application:get_env(onomondo_eim, rest_timeout_stuck), - {ok, RestTimeoutNoshow} = application:get_env(onomondo_eim, rest_timeout_noshow), - {ok, RestTimeoutExpired} = application:get_env(onomondo_eim, rest_timeout_expired), - RcStalled = mark_stuck(RestTimeoutStuck), - RcNoshow = mark_noshow(RestTimeoutNoshow), - RcExpired = delete_expired(RestTimeoutExpired), - - case {RcStalled, RcNoshow, RcExpired} of - {ok, ok, ok} -> - ok; - _ -> - logger:error("Cleanup: database error~n"), - error - end, - - % Next cleanup in 10 secs. - {ok, _} = timer:apply_after(10000, mnesia_db, cleanup, []), - ok. - -% Run scheduled eUICC procedures -euicc_setparam() -> - % An eUICC procedure in the context of this module has nothing to do with any of the procedures specified in - % GSMA SGP.22 or SGP.32. In this module an eUICC procedure is a virtual procedure were parameters in the - % euicc table are set. - - HandleParam = fun(ResourceId, EidValue, Param) -> - case Param of - {[{Name, Value}]} -> - case trans_update_euicc_param(EidValue, binary_to_atom(Name), Value) of - ok -> - trans_rest_set_status( - ResourceId, - done, - [{[{euiccUpdateResult, ok}]}], - none - ); - _ -> - trans_rest_set_status( - ResourceId, - done, - [{[{euiccUpdateResult, badParam}]}], - none - ) - end; - _ -> - trans_rest_set_status( - ResourceId, - done, - [{[{euiccUpdateResult, badParamFormat}]}], - none - ) - end - end, - - % Parse order and process each parameter individually - HandleResource = fun({ResourceId, EidValue, Order}) -> - trans_euicc_create_if_not_exist(EidValue), - case Order of - {[{<<"euicc">>, ParameterList}]} -> - [HandleParam(ResourceId, EidValue, Param) || Param <- ParameterList], - ok; - _ -> - trans_rest_set_status(ResourceId, done, [{[{procedureError, badOrder}]}], none) - end - end, - - % Look into facility euicc and find the first entry that is in status "new". - Trans = fun() -> - Q = qlc:q([ - {X#rest.resourceId, X#rest.eidValue, X#rest.order} - || X <- mnesia:table(rest), X#rest.status == new, X#rest.facility == euicc - ]), - Rows = qlc:e(Q), - case Rows of - [] -> - ok; - Rows -> - [HandleResource(Row) || Row <- Rows], - ok - end - end, - - {atomic, Result} = mnesia:transaction(Trans), - case Result of - ok -> - ok; - _ -> - logger:error("eUICC: euicc procedure failed, database error~n"), - error - end, - - % Next euicc procedure in 10 secs. - {ok, _} = timer:apply_after(10000, mnesia_db, euicc_setparam, []), - ok. - -% Dump all currently pending rest items (for debugging, to be called from console) -dump_rest() -> - Trans = fun() -> - Q = qlc:q([X || X <- mnesia:table(rest)]), - qlc:e(Q) - end, - {atomic, Result} = mnesia:transaction(Trans), - Result. - -% Dump all currently pending work items (for debugging, to be called from console) -dump_work() -> - Trans = fun() -> - Q = qlc:q([X || X <- mnesia:table(work)]), - qlc:e(Q) - end, - {atomic, Result} = mnesia:transaction(Trans), - Result. - -% Dump all eUICCs we are aware of -dump_euicc() -> - Trans = fun() -> - Q = qlc:q([X || X <- mnesia:table(euicc)]), - qlc:e(Q) - end, - {atomic, Result} = mnesia:transaction(Trans), - Result. diff --git a/src/mnesia_db_euicc.erl b/src/mnesia_db_euicc.erl new file mode 100644 index 0000000..099b615 --- /dev/null +++ b/src/mnesia_db_euicc.erl @@ -0,0 +1,257 @@ +% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved. +% +% SPDX-License-Identifier: AGPL-3.0-only +% +% Author: Philipp Maier pmaier@sysmocom.de / sysmocom - s.f.m.c. GmbH + +-module(mnesia_db_euicc). +-include_lib("stdlib/include/qlc.hrl"). +-include("mnesia_db_rest.hrl"). +-include("mnesia_db_euicc.hrl"). + +% euicc functions, to be called by the eIM code (from inside) +-export([counter_tick/1, param_get/2, param_set/3, create_if_not_exist/1]). + +% debugging +-export([dump/0]). + +% trigger recurring events (called automatically by timer from this module) +-export([timer_setparam/0]). + +trans_create_if_not_exist(EidValue) -> + {ok, CounterValue} = application:get_env(onomondo_eim, counter_value), + {ok, ConsumerEuicc} = application:get_env(onomondo_eim, consumer_euicc), + Row = #euicc{ + eidValue = EidValue, + counterValue = CounterValue, + consumerEuicc = ConsumerEuicc, + associationToken = 1, + signPubKey = <<>>, + signAlgo = <<"prime256v1">> + }, + Q = qlc:q([X#euicc.eidValue || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), + Present = qlc:e(Q), + case Present of + [] -> + mnesia:write(Row); + _ -> + present + end. + +% Create a new eUICC master data entry +create_if_not_exist(EidValue) -> + Trans = fun() -> + trans_create_if_not_exist(EidValue) + end, + {atomic, Result} = mnesia:transaction(Trans), + case Result of + ok -> + logger:info("eUICC: creating new master data entry,~neID=~p~n", [EidValue]), + ok; + present -> + ok; + _ -> + logger:error("eUICC: cannot create master data entry, database error,~neID=~p~n", [ + EidValue + ]), + error + end. + +% get an incremented counterValue (and store the incremented counterValue as the current counterValue) +counter_tick(EidValue) -> + Trans = fun() -> + Q = qlc:q([X || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), + Rows = qlc:e(Q), + case Rows of + [Row | _] -> + CounterValue = Row#euicc.counterValue + 1, + ok = mnesia:write(Row#euicc{counterValue = CounterValue}), + {ok, CounterValue}; + [] -> + error; + _ -> + error + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + case Result of + {ok, CounterValue} -> + logger:info("eUICC: incrementing counterValue,~neID=~p, counter=~p~n", [ + EidValue, CounterValue + ]), + {ok, CounterValue}; + _ -> + logger:error("eUICC: cannot increment counterValue, database error,~neID=~p~n", [ + EidValue + ]), + error + end. + +%update one specific parameter in the euicc table +trans_update_param(EidValue, Name, Value) -> + Q = qlc:q([X || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), + Rows = qlc:e(Q), + case Rows of + [Row | []] -> + case Name of + counterValue -> + mnesia:write(Row#euicc{counterValue = Value}); + consumerEuicc -> + mnesia:write(Row#euicc{consumerEuicc = Value}); + associationToken -> + mnesia:write(Row#euicc{associationToken = Value}); + signPubKey -> + mnesia:write(Row#euicc{signPubKey = Value}); + signAlgo -> + mnesia:write(Row#euicc{signAlgo = Value}); + _ -> + error + end; + [] -> + error; + _ -> + error + end. + +% Get an eUICC parameter by its name (atom) +param_get(EidValue, Name) -> + Trans = fun() -> + Q = qlc:q([X || X <- mnesia:table(euicc), X#euicc.eidValue == EidValue]), + Rows = qlc:e(Q), + case Rows of + [Row | _] -> + case Name of + counterValue -> + {ok, Row#euicc.counterValue}; + consumerEuicc -> + {ok, Row#euicc.consumerEuicc}; + associationToken -> + {ok, Row#euicc.associationToken}; + signPubKey -> + {ok, Row#euicc.signPubKey}; + signAlgo -> + {ok, Row#euicc.signAlgo}; + _ -> + error + end; + [] -> + error; + _ -> + error + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + case Result of + {ok, Value} -> + logger:info("eUICC: reading eUICC parameter,~neID=~p, name=~p, value=~p~n", [ + EidValue, Name, Value + ]), + {ok, Value}; + _ -> + logger:error("eUICC: cannot read eUICC parameter,~neID=~p, name=~p~n", [EidValue, Name]), + error + end. + +% Update an eUICC parameter by its name (atom) +param_set(EidValue, Name, Value) -> + Trans = fun() -> + trans_update_param(EidValue, Name, Value) + end, + {atomic, Result} = mnesia:transaction(Trans), + case Result of + ok -> + logger:info("eUICC: writing eUICC parameter,~neID=~p, name=~p, value=~p~n", [ + EidValue, Name, Value + ]), + ok; + _ -> + logger:error("eUICC: cannot write eUICC parameter,~neID=~p, name=~p~n", [EidValue, Name]), + error + end. + +% Run scheduled eUICC procedures +timer_setparam() -> + % An eUICC procedure in the context of this module has nothing to do with any of the procedures specified in + % GSMA SGP.22 or SGP.32. In this module an eUICC procedure is a virtual procedure were parameters in the + % euicc table are set. + + HandleParam = fun(ResourceId, EidValue, Param) -> + case Param of + {[{Name, Value}]} -> + case trans_update_param(EidValue, binary_to_atom(Name), Value) of + ok -> + mnesia_db_rest:trans_set_status( + ResourceId, + done, + [{[{euiccUpdateResult, ok}]}], + none + ); + _ -> + mnesia_db_rest:trans_set_status( + ResourceId, + done, + [{[{euiccUpdateResult, badParam}]}], + none + ) + end; + _ -> + mnesia_db_rest:trans_set_status( + ResourceId, + done, + [{[{euiccUpdateResult, badParamFormat}]}], + none + ) + end + end, + + % Parse order and process each parameter individually + HandleResource = fun({ResourceId, EidValue, Order}) -> + trans_create_if_not_exist(EidValue), + case Order of + {[{<<"euicc">>, ParameterList}]} -> + [HandleParam(ResourceId, EidValue, Param) || Param <- ParameterList], + ok; + _ -> + mnesia_db_rest:trans_set_status(ResourceId, done, [{[{procedureError, badOrder}]}], none) + end + end, + + % Look into facility euicc and find the first entry that is in status "new". + Trans = fun() -> + Q = qlc:q([ + {X#rest.resourceId, X#rest.eidValue, X#rest.order} + || X <- mnesia:table(rest), X#rest.status == new, X#rest.facility == euicc + ]), + Rows = qlc:e(Q), + case Rows of + [] -> + ok; + Rows -> + [HandleResource(Row) || Row <- Rows], + ok + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + case Result of + ok -> + ok; + _ -> + logger:error("eUICC: euicc procedure failed, database error~n"), + error + end, + + % Next euicc procedure in 10 secs. + {ok, _} = timer:apply_after(10000, mnesia_db_euicc, timer_setparam, []), + ok. + +% Dump all eUICCs we are aware of +dump() -> + Trans = fun() -> + Q = qlc:q([X || X <- mnesia:table(euicc)]), + qlc:e(Q) + end, + {atomic, Result} = mnesia:transaction(Trans), + Result. diff --git a/src/mnesia_db_rest.erl b/src/mnesia_db_rest.erl new file mode 100644 index 0000000..6a51567 --- /dev/null +++ b/src/mnesia_db_rest.erl @@ -0,0 +1,286 @@ +% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved. +% +% SPDX-License-Identifier: AGPL-3.0-only +% +% Author: Philipp Maier pmaier@sysmocom.de / sysmocom - s.f.m.c. GmbH + +-module(mnesia_db_rest). +-include_lib("stdlib/include/qlc.hrl"). +-include("mnesia_db_rest.hrl"). +-include("mnesia_db_work.hrl"). + +% REST functions, to be called by the REST server (from outside via cowboy) +-export([list/1, lookup/2, create/3, delete/2]). + +% debugging +-export([dump/0]). + +% trigger recurring events (called automatically by timer from this module) +-export([timer_cleanup/0]). + +% transaction functions (to be called from a transaction) +-export([trans_set_status/4]). + +% helper function (to be called from a transaction) to set the status of an item in the rest table. This function +% is intended to be called from mnesia_db_work to report back the status of a specific work item. +trans_set_status(ResourceId, Status, Outcome, Debuginfo) -> + Q = qlc:q([X || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]), + Rows = qlc:e(Q), + Timestamp = os:system_time(seconds), + case Rows of + [Row | []] -> + mnesia:write(Row#rest{ + status = Status, timestamp = Timestamp, outcome = Outcome, debuginfo = Debuginfo + }), + ok; + [] -> + error; + _ -> + error + end. + +% Create REST resource (order) +create(Facility, EidValue, Order) -> + ok = mnesia_db_euicc:create_if_not_exist(EidValue), + ResourceId = uuid:uuid_to_string(uuid:get_v4_urandom()), + Timestamp = os:system_time(seconds), + Row = #rest{ + resourceId = ResourceId, + facility = Facility, + eidValue = EidValue, + order = Order, + status = new, + timestamp = Timestamp, + outcome = [], + debuginfo = none + }, + Trans = fun() -> + Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]), + Present = qlc:e(Q), + case Present of + [] -> + mnesia:write(Row); + _ -> + error + end + end, + {atomic, Result} = mnesia:transaction(Trans), + {Result, ResourceId}. + +% Lookup REST resource (order) +lookup(ResourceId, Facility) -> + Trans = fun() -> + Q = qlc:q([ + { + X#rest.status, + X#rest.timestamp, + X#rest.eidValue, + X#rest.order, + X#rest.outcome, + X#rest.debuginfo + } + || X <- mnesia:table(rest), + X#rest.resourceId == ResourceId, + X#rest.facility == Facility + ]), + qlc:e(Q) + end, + {atomic, Result} = mnesia:transaction(Trans), + case Result of + [{Status, Timestamp, EidValue, Order, Outcome, Debuginfo}] -> + {Status, Timestamp, EidValue, Order, Outcome, Debuginfo}; + [] -> + none; + _ -> + error + end. + +% Delete REST resource (order) +delete(ResourceId, Facility) -> + Trans = fun() -> + QRest = qlc:q([ + X#rest.resourceId + || X <- mnesia:table(rest), + X#rest.resourceId == ResourceId, + X#rest.facility == Facility + ]), + RestPresent = qlc:e(QRest), + case RestPresent of + [] -> + none; + _ -> + OidRest = {rest, ResourceId}, + ok = mnesia:delete(OidRest), + + % There may be an orphaned work item now, which we must also remove. This will also kill + % the order in case it is currently in progress. + QWork = qlc:q([ + X#work.pid + || X <- mnesia:table(work), X#work.resourceId == ResourceId + ]), + WorkPresent = qlc:e(QWork), + case WorkPresent of + [] -> + ok; + [Pid] -> + OidWork = {work, Pid}, + mnesia:delete(OidWork); + _ -> + error + end + end + end, + {atomic, Result} = mnesia:transaction(Trans), + Result. + +% List the resource identifiers of currently present REST resources +list(Facility) -> + Trans = fun() -> + Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.facility == Facility]), + qlc:e(Q) + end, + {atomic, Result} = mnesia:transaction(Trans), + Result. + +mark_stuck(Timeout) -> + % There may be corner cases where the order gets stuck because the other entity suddenly stops responding. In + % this case the order will stall. When it remains stalled for too long (minutes), we should remove all related + % items from the work table and put an appropriate outcome (error code) into the rest table. + + TimestampNow = os:system_time(seconds), + + % Remove Resource from work table and set an appropriate status in the rest table + HandleResource = fun(ResourceId) -> + % find the pid of the work item that is stuck and then delete it + Q = qlc:q([X#work.pid || X <- mnesia:table(work), X#work.resourceId == ResourceId]), + WorkPresent = qlc:e(Q), + case WorkPresent of + [Pid] -> + Oid = {work, Pid}, + ok = mnesia:delete(Oid); + _ -> + ok + end, + + % set status in the rest table + trans_set_status(ResourceId, done, [{[{procedureError, stuckOrder}]}], none) + end, + + % Find all rest resources that stall in status "work" and older than the specified timeout value + Trans = fun() -> + Q = qlc:q([ + X#rest.resourceId + || X <- mnesia:table(rest), + X#rest.status == work, + TimestampNow - X#rest.timestamp > Timeout + ]), + Rows = qlc:e(Q), + case Rows of + [] -> + ok; + Rows -> + [HandleResource(Row) || Row <- Rows], + ok + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + Result. + +mark_noshow(Timeout) -> + % There may be corner cases where the order is never processed because the related IPAd/eUICC never shows up to + % fetch the related eUICC package. If we see an order staying in status "new" for too long (days, weeks), we should + % mark it as "done" and put an appropriate outcome (error code) into the rest table. + + TimestampNow = os:system_time(seconds), + + % Remove Resource from work table and set an appropriate status in the rest table + HandleResource = fun(ResourceId) -> + trans_set_status(ResourceId, done, [{[{procedureError, noshowOrder}]}], none) + end, + + % Find all rest resources that stall in status "work" and older than the specified timeout value + Trans = fun() -> + Q = qlc:q([ + X#rest.resourceId + || X <- mnesia:table(rest), + X#rest.status == new, + TimestampNow - X#rest.timestamp > Timeout + ]), + Rows = qlc:e(Q), + case Rows of + [] -> + ok; + Rows -> + [HandleResource(Row) || Row <- Rows], + ok + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + Result. + +delete_expired(Timeout) -> + % There may be cases where orders stay unmaintained for too long. When an order stays in status "done" for too long + % (hours, days), than this may mean that the REST API user lost interest. In this case the related items shoud be + % removed from the rest table after a reasonable timeout. + + TimestampNow = os:system_time(seconds), + + % Remove Resource from the rest table. Since an abandonned order won't have a coresponding work item in the work + % table we do not have to worry about creating an orphaned work item. + HandleResource = fun(ResourceId) -> + Oid = {rest, ResourceId}, + ok = mnesia:delete(Oid) + end, + + % Find all rest resources that linger in the rest table for a long time and are not in the status "new" + Trans = fun() -> + Q = qlc:q([ + X#rest.resourceId + || X <- mnesia:table(rest), + X#rest.status == done, + TimestampNow - X#rest.timestamp > Timeout + ]), + Rows = qlc:e(Q), + case Rows of + [] -> + ok; + Rows -> + [HandleResource(Row) || Row <- Rows], + ok + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + Result. + +% Run a cleanup cycle the database +timer_cleanup() -> + {ok, RestTimeoutStuck} = application:get_env(onomondo_eim, rest_timeout_stuck), + {ok, RestTimeoutNoshow} = application:get_env(onomondo_eim, rest_timeout_noshow), + {ok, RestTimeoutExpired} = application:get_env(onomondo_eim, rest_timeout_expired), + RcStalled = mark_stuck(RestTimeoutStuck), + RcNoshow = mark_noshow(RestTimeoutNoshow), + RcExpired = delete_expired(RestTimeoutExpired), + + case {RcStalled, RcNoshow, RcExpired} of + {ok, ok, ok} -> + ok; + _ -> + logger:error("Cleanup: database error~n"), + error + end, + + % Next cleanup in 10 secs. + {ok, _} = timer:apply_after(10000, mnesia_db_rest, timer_cleanup, []), + ok. + +% Dump all currently pending rest items (for debugging, to be called from console) +dump() -> + Trans = fun() -> + Q = qlc:q([X || X <- mnesia:table(rest)]), + qlc:e(Q) + end, + {atomic, Result} = mnesia:transaction(Trans), + Result. diff --git a/src/mnesia_db_work.erl b/src/mnesia_db_work.erl new file mode 100644 index 0000000..9a33ebd --- /dev/null +++ b/src/mnesia_db_work.erl @@ -0,0 +1,273 @@ +% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved. +% +% SPDX-License-Identifier: AGPL-3.0-only +% +% Author: Philipp Maier pmaier@sysmocom.de / sysmocom - s.f.m.c. GmbH + +-module(mnesia_db_work). +-include_lib("stdlib/include/qlc.hrl"). +-include("mnesia_db_rest.hrl"). +-include("mnesia_db_work.hrl"). + +% work functions, to be called by the eIM code (from inside) +-export([fetch/2, pickup/2, update/2, bind/2, finish/3]). + +% debugging +-export([dump/0]). + +% Start working on an order by creating a an entry in the work table and marking it's status as "work". After calling +% this it is the callers responsibility to handle the work item and call rest_finish_order when the work is done. +fetch(EidValue, Pid) -> + % One process can only work on one work item at a time. The API user must call finish before the next work + % item can be processed. If there is already a pending work item under the given PID, forcefully finish this work + % item. + TransPidExists = fun() -> + Q = qlc:q([X || X <- mnesia:table(work), X#work.pid == Pid]), + WorkPresent = qlc:e(Q), + case WorkPresent of + [] -> + false; + _ -> + true + end + end, + + {atomic, PidExists} = mnesia:transaction(TransPidExists), + case PidExists of + true -> + finish(Pid, [{[{procedureError, stuckOrder}]}], none); + false -> + ok + end, + + % Read the next pending REST resource from the rest table and create a related work item. The work item is then + % in progress. + Trans = fun() -> + Q = qlc:q([ + X + || X <- mnesia:table(rest), + X#rest.eidValue == EidValue, + X#rest.status == new, + X#rest.facility =/= euicc + ]), + Rows = qlc:e(Q), + case Rows of + [Row | _] -> + % Create an entry in the work table + WorkRow = #work{ + pid = Pid, + resourceId = Row#rest.resourceId, + transactionId = none, + eidValue = Row#rest.eidValue, + order = Row#rest.order, + state = none + }, + case mnesia:write(WorkRow) of + ok -> + % We are now working on this order + ok = mnesia_db_rest:trans_set_status(Row#rest.resourceId, work, [], none), + Row; + _ -> + error + end; + [] -> + none; + _ -> + error + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + case Result of + {rest, _, Facility, _, Order, _, _, _, _} -> + logger:info( + "Work: fetching new work item,~nEidValue=~p, Pid=~p, Facility=~p,~nOrder=~p~n", + [EidValue, Pid, Facility, Order] + ), + {Facility, Order}; + none -> + logger:info("Work: no work item in database,~nEidValue=~p, Pid=~p~n", [EidValue, Pid]), + none; + _ -> + logger:error("Work: cannot fetch work item, database error,~nEidValue=~p, Pid=~p~n", [ + EidValue, Pid + ]), + error + end. + +% Bind a work item to a TransactionId. The transactionId has to be a unique identifier that can be used as a secondary +% key to find a work item in the databse. The binding works in two directions. The work item is first searched by its +% pid, when found, the transactionId is updated. In case the pid has become invalid, then the work item is searched +% again by the transactionId and when found, the pid is updated. This function can be called any time after fetch +% was called before. It can also be called multiple times. +bind(Pid, TransactionId) -> + % Transaction to update the TransactionId. This is the normal case. A work item starts without having a + % TransactionId assigned. As soon as a (new) TransactionId becomes known, it is updated using this Transaction. + TransUpdateTrnsId = fun() -> + Q = qlc:q([X || X <- mnesia:table(work), X#work.pid == Pid]), + Rows = qlc:e(Q), + case Rows of + [Row | _] -> + mnesia:write(Row#work{transactionId = TransactionId}); + [] -> + none; + _ -> + error + end + end, + + % Transaction to update the PID. This is a corner case that comes into play in case the PID is lost (the + % process/connection handling this work item has died). We then try to find the work item by the TransactionId + % and update its PID. + TransUpdatePid = fun() -> + Q = qlc:q([X || X <- mnesia:table(work), X#work.transactionId == TransactionId]), + Rows = qlc:e(Q), + case Rows of + [Row | _] -> + mnesia:write(Row#work{pid = Pid}); + [] -> + none; + _ -> + error + end + end, + + {atomic, Result} = mnesia:transaction(TransUpdateTrnsId), + case Result of + ok -> + logger:info("Work: bound work item to transactionId,~nPid=~p, TransactionId=~p~n", [ + Pid, TransactionId + ]), + ok; + none -> + {atomic, UpdatePidResult} = mnesia:transaction(TransUpdatePid), + case UpdatePidResult of + ok -> + logger:info("Work: bound work item to PID,~nPid=~p, TransactionId=~p~n", [ + Pid, TransactionId + ]), + ok; + none -> + logger:error( + "Work: cannot bind work item, transactionId nor PID found,~nPid=~p, TransactionId=~p~n", + [Pid, TransactionId] + ), + error; + _ -> + logger:error( + "Work: cannot bind work item, database error,~nPid=~p, TransactionId=~p, TransUpdatePid~n", + [Pid, TransactionId] + ), + error + end; + _ -> + logger:error( + "Work: cannot bind work item, database error,~nPid=~p, TransactionId=~p, TransUpdateTrnsId~n", + [Pid, TransactionId] + ), + error + end. + +% Pickup a work item that is in progress. This function can be called any time after fetch was called +% before. It can also be called multiple times. +pickup(Pid, TransactionId) -> + % In case a TransactionId is provied, bind the PID to this TransactionId, + WorkBound = + case TransactionId of + none -> + ok; + _ -> + bind(Pid, TransactionId) + end, + + % Lookup the work state by the given PID + case WorkBound of + ok -> + Trans = fun() -> + Q = qlc:q([ + {X#work.eidValue, X#work.order, X#work.state} + || X <- mnesia:table(work), X#work.pid == Pid + ]), + qlc:e(Q) + end, + {atomic, Result} = mnesia:transaction(Trans), + case Result of + [{EidValue, Order, State} | _] -> + {EidValue, Order, State}; + [] -> + logger:error( + "Work: no work item found under specified Pid, already finished?, not fetched?,~nPid=~p~n", + [Pid] + ), + none; + _ -> + logger:error("Work: cannot pick up work item, database error,~nPid=~p~n", [Pid]), + error + end; + _ -> + WorkBound + end. + +% Update a work item that is in progress. This fuction updates the state (any user defined term) of the work item. +% This function can be called any time after fetch was called before. It can also be called multiple times. +update(Pid, State) -> + Trans = fun() -> + Q = qlc:q([X || X <- mnesia:table(work), X#work.pid == Pid]), + Rows = qlc:e(Q), + case Rows of + [Row | _] -> + mnesia:write(Row#work{state = State}); + [] -> + error; + _ -> + error + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + case Result of + ok -> + logger:info("Work: updating work item,~nPid=~p, State=~p~n", [Pid, State]), + ok; + _ -> + logger:error("Work: cannot update, database error,~nPid=~p, State=~p~n", [Pid, State]), + error + end. + +% Finish an order that has been worked on. This removes the related entry from the work table and sets the status in +% the rest table to "done". +finish(Pid, Outcome, Debuginfo) -> + Trans = fun() -> + Q = qlc:q([X#work.resourceId || X <- mnesia:table(work), X#work.pid == Pid]), + Rows = qlc:e(Q), + case Rows of + [] -> + error; + [ResourceId | _] -> + Oid = {work, Pid}, + ok = mnesia:delete(Oid), + ok = mnesia_db_rest:trans_set_status(ResourceId, done, Outcome, Debuginfo) + end + end, + + {atomic, Result} = mnesia:transaction(Trans), + case Result of + ok -> + logger:info("Work: finishing work item,~nPid=~p, Outcome=~p~n", [Pid, Outcome]), + ok; + _ -> + logger:error( + "Work: cannot finish work item, database error,~nPid=~p, Outcome=~p~n", + [Pid, Outcome] + ), + error + end. + +% Dump all currently pending work items (for debugging, to be called from console) +dump() -> + Trans = fun() -> + Q = qlc:q([X || X <- mnesia:table(work)]), + qlc:e(Q) + end, + {atomic, Result} = mnesia:transaction(Trans), + Result. diff --git a/src/rest_handler.erl b/src/rest_handler.erl index db3f048..9f5ab4d 100644 --- a/src/rest_handler.erl +++ b/src/rest_handler.erl @@ -69,7 +69,7 @@ ContentDecoded = jiffy:decode(Content), {[{<<"eidValue">>, EidValue}, _]} = ContentDecoded, {[_, {<<"order">>, Order}]} = ContentDecoded, - {ok, ResourceId} = mnesia_db:rest_create(Facility, EidValue, Order), + {ok, ResourceId} = mnesia_db_rest:create(Facility, EidValue, Order), case cowboy_req:method(Req1) of <<"POST">> -> Response = io_lib:format("/~s/lookup/~s", [Facility, ResourceId]), @@ -89,7 +89,7 @@ "REST: client requests REST resource for lookup,~nPeer=~p, ResourceId=~p, Facility=~p~n", [maps:get(peer, Req), ResourceId, Facility] ), - Result = mnesia_db:rest_lookup(ResourceId, Facility), + Result = mnesia_db_rest:lookup(ResourceId, Facility), Response = case Result of {Status, Timestamp, EidValue, Order, Outcome, Debuginfo} -> @@ -124,7 +124,7 @@ "REST: client requests REST resource for delete,~nPeer=~p, ResourceId=~p, Facility=~p~n", [maps:get(peer, Req), ResourceId, Facility] ), - Result = mnesia_db:rest_delete(ResourceId, Facility), + Result = mnesia_db_rest:delete(ResourceId, Facility), Response = case Result of ok -> @@ -147,7 +147,7 @@ "REST: client requests REST resource list,~nPeer=~p, Facility=~p~n", [maps:get(peer, Req), Facility] ), - Result = mnesia_db:rest_list(Facility), + Result = mnesia_db_rest:list(Facility), ResourceIdList = [list_to_binary(X) || X <- Result], Response = io_lib:format( "{"resourceIdList": ~s}",