dexter has uploaded this change for review.

View Change

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}",

To view, visit change 42946. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-MessageType: newchange
Gerrit-Project: onomondo-eim
Gerrit-Branch: master
Gerrit-Change-Id: Ifc649337ab7ce7eb9dc602a1439293130f002f41
Gerrit-Change-Number: 42946
Gerrit-PatchSet: 1
Gerrit-Owner: dexter <pmaier@sysmocom.de>