fixeria has submitted this change. ( https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/41100?usp=email )
Change subject: [REST] Implement Pfcp{AssocState,Heartbeat} ......................................................................
[REST] Implement Pfcp{AssocState,Heartbeat}
Change-Id: Idc98952d46d8e224969da343dc29ef323c6ed813 Related: SYS#7066 --- M contrib/openapi.yaml M contrib/osmo-s1gw-cli.py M doc/osmo-s1gw-cli.md M priv/openapi.json M src/rest_server.erl 5 files changed, 307 insertions(+), 1 deletion(-)
Approvals: Jenkins Builder: Verified osmith: Looks good to me, but someone else must approve laforge: Looks good to me, but someone else must approve fixeria: Looks good to me, approved
diff --git a/contrib/openapi.yaml b/contrib/openapi.yaml index 696e7a2..df079a9 100644 --- a/contrib/openapi.yaml +++ b/contrib/openapi.yaml @@ -32,8 +32,61 @@ schema: $ref: '#/components/schemas/MetricsList'
+ /pfcp/assoc: + get: + summary: Get the PFCP association state + operationId: PfcpAssocState + responses: + '200': + description: PFCP association state + content: + application/json: + schema: + $ref: '#/components/schemas/PfcpAssocState' + post: + summary: Initiate the PFCP Association Setup procedure + operationId: PfcpAssocSetup + responses: + '200': + $ref: '#/components/responses/OperationResult' + delete: + summary: Initiate the PFCP Association Release procedure + operationId: PfcpAssocRelease + responses: + '200': + $ref: '#/components/responses/OperationResult' + + /pfcp/heartbeat: + post: + summary: Send a PFCP Heartbeat Request to the peer + operationId: PfcpHeartbeat + responses: + '200': + $ref: '#/components/responses/OperationResult' + components: + responses: + OperationResult: + description: Generic operation result response + content: + application/json: + schema: + $ref: '#/components/schemas/OperationResult' + schemas: + OperationResult: + type: object + required: + - success + properties: + success: + type: boolean + description: Indicates whether the operation was successful + message: + type: string + nullable: true + description: Human-readable explanation of the result + MetricsList: type: array items: @@ -53,3 +106,27 @@ type: string value: type: integer + + PfcpAssocState: + type: object + required: + - state + - laddr + - raddr + properties: + state: + type: string + enum: [connecting, connected] + description: Current association state + laddr: + type: string + description: Local (bind) IP address + raddr: + type: string + description: Remote (connect) IP address + lrts: + type: integer + description: Local Recovery TimeStamp + rrts: + type: integer + description: Remote Recovery TimeStamp diff --git a/contrib/osmo-s1gw-cli.py b/contrib/osmo-s1gw-cli.py index 76c444e..77014ad 100755 --- a/contrib/osmo-s1gw-cli.py +++ b/contrib/osmo-s1gw-cli.py @@ -82,11 +82,32 @@ with self.send_get_req('metrics-list', query) as f: return json.load(f)
+ def pfcp_assoc_state(self) -> RESTResponse: + ''' PfcpAssocState :: Get the PFCP association state ''' + with self.send_get_req('pfcp/assoc') as f: + return json.load(f) + + def pfcp_assoc_setup(self) -> RESTResponse: + ''' PfcpAssocSetup :: Initiate the PFCP Association Setup procedure ''' + with self.send_post_req('pfcp/assoc') as f: + return json.load(f) + + def pfcp_assoc_release(self) -> RESTResponse: + ''' PfcpAssocRelease :: Initiate the PFCP Association Release procedure ''' + with self.send_delete_req('pfcp/assoc') as f: + return json.load(f) + + def pfcp_heartbeat(self) -> RESTResponse: + ''' PfcpHeartbeat :: Send a PFCP Heartbeat Request to the peer ''' + with self.send_post_req('pfcp/heartbeat') as f: + return json.load(f) +
class OsmoS1GWCli(cmd2.Cmd): DESC = 'Interactive CLI for OsmoS1GW'
CAT_METRICS = 'Metrics commands' + CAT_PFCP = 'PFCP related commands'
def __init__(self, argv): super().__init__(allow_cli_args=False, include_py=True) @@ -136,6 +157,40 @@ self.poutput(tabulate.tabulate(map(self.metrics_list_item, data), headers='keys', tablefmt=self.tablefmt))
+ @cmd2.with_category(CAT_PFCP) + def do_pfcp_assoc_state(self, opts) -> None: + ''' Get the PFCP association state ''' + data = self.iface.pfcp_assoc_state() + table = [] # [param, value] + table.append(['State', data['state']]) + table.append(['Local address', data['laddr']]) + table.append(['Remote address', data['raddr']]) + table.append(['Local Recovery TimeStamp', data['lrts']]) + if 'rrts' in data: + table.append(['Remote Recovery TimeStamp', data['rrts']]) + self.poutput(tabulate.tabulate(table, + headers=['Parameter', 'Value'], + tablefmt=self.tablefmt)) + + @cmd2.with_category(CAT_PFCP) + def do_pfcp_assoc_setup(self, opts) -> None: + ''' Initiate the PFCP Association Setup procedure ''' + raise NotImplementedError + + @cmd2.with_category(CAT_PFCP) + def do_pfcp_assoc_release(self, opts) -> None: + ''' Initiate the PFCP Association Release procedure ''' + raise NotImplementedError + + @cmd2.with_category(CAT_PFCP) + def do_pfcp_heartbeat(self, opts) -> None: + ''' Send a PFCP Heartbeat Request ''' + data = self.iface.pfcp_heartbeat() + if data['success']: + self.poutput('Heartbeat succeeded') + else: + self.perror('Heartbeat failed: {message}'.format(**data)) +
ap = argparse.ArgumentParser(prog='osmo-s1gw-cli', description=OsmoS1GWCli.DESC)
diff --git a/doc/osmo-s1gw-cli.md b/doc/osmo-s1gw-cli.md index d5ceb94..64298ab 100644 --- a/doc/osmo-s1gw-cli.md +++ b/doc/osmo-s1gw-cli.md @@ -124,3 +124,29 @@ | s1ap.enb.num_sctp_connections | gauge | 0 | | s1ap.proxy.uplink_packets_queued | gauge | 0 | ``` + +### `pfcp_assoc_state` + +Get the PFCP association state. + +``` +| Parameter | Value | +|---------------------------|------------| +| State | connected | +| Local address | 127.0.3.1 | +| Remote address | 127.0.3.10 | +| Local Recovery TimeStamp | 3967211233 | +| Remote Recovery TimeStamp | 3965211123 | +``` + +### `pfcp_heartbeat` + +Send a PFCP Heartbeat Request. + +``` +OsmoS1GW# pfcp_heartbeat +Heartbeat succeeded + +OsmoS1GW# pfcp_heartbeat +Heartbeat failed: timeout +``` diff --git a/priv/openapi.json b/priv/openapi.json index 7323e9a..c55c312 100644 --- a/priv/openapi.json +++ b/priv/openapi.json @@ -49,10 +49,86 @@ } } } + }, + "/pfcp/assoc": { + "get": { + "summary": "Get the PFCP association state", + "operationId": "PfcpAssocState", + "responses": { + "200": { + "description": "PFCP association state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PfcpAssocState" + } + } + } + } + } + }, + "post": { + "summary": "Initiate the PFCP Association Setup procedure", + "operationId": "PfcpAssocSetup", + "responses": { + "200": { + "$ref": "#/components/responses/OperationResult" + } + } + }, + "delete": { + "summary": "Initiate the PFCP Association Release procedure", + "operationId": "PfcpAssocRelease", + "responses": { + "200": { + "$ref": "#/components/responses/OperationResult" + } + } + } + }, + "/pfcp/heartbeat": { + "post": { + "summary": "Send a PFCP Heartbeat Request to the peer", + "operationId": "PfcpHeartbeat", + "responses": { + "200": { + "$ref": "#/components/responses/OperationResult" + } + } + } } }, "components": { + "responses": { + "OperationResult": { + "description": "Generic operation result response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationResult" + } + } + } + } + }, "schemas": { + "OperationResult": { + "type": "object", + "required": [ + "success" + ], + "properties": { + "success": { + "type": "boolean", + "description": "Indicates whether the operation was successful" + }, + "message": { + "type": "string", + "nullable": true, + "description": "Human-readable explanation of the result" + } + } + }, "MetricsList": { "type": "array", "items": { @@ -81,6 +157,40 @@ "type": "integer" } } + }, + "PfcpAssocState": { + "type": "object", + "required": [ + "state", + "laddr", + "raddr" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "connecting", + "connected" + ], + "description": "Current association state" + }, + "laddr": { + "type": "string", + "description": "Local (bind) IP address" + }, + "raddr": { + "type": "string", + "description": "Remote (connect) IP address" + }, + "lrts": { + "type": "integer", + "description": "Local Recovery TimeStamp" + }, + "rrts": { + "type": "integer", + "description": "Remote Recovery TimeStamp" + } + } } } } diff --git a/src/rest_server.erl b/src/rest_server.erl index ee0a7de..d1597a9 100644 --- a/src/rest_server.erl +++ b/src/rest_server.erl @@ -34,7 +34,9 @@
-module(rest_server).
--export([metrics_list/1 +-export([metrics_list/1, + pfcp_assoc_state/1, + pfcp_heartbeat/1 ]).
-include_lib("kernel/include/logger.hrl"). @@ -60,6 +62,28 @@ {200, [], L1}.
+%% PfcpAssocState :: Get the PFCP association state +pfcp_assoc_state(#{}) -> + Info0 = pfcp_peer:fetch_info(), + Info1 = maps:update_with(laddr, fun inet:ntoa/1, Info0), + Info2 = maps:update_with(raddr, fun inet:ntoa/1, Info1), + {200, [], rsp_map(Info2)}. + +%% TODO: PfcpAssocSetup :: Initiate the PFCP Association Setup procedure +%% TODO: PfcpAssocRelease :: Initiate the PFCP Association Release procedure + + +%% PfcpHeartbeat :: Send a PFCP Heartbeat Request to the peer +pfcp_heartbeat(#{}) -> + case pfcp_peer:heartbeat_req() of + ok -> + {200, [], rsp_map(#{success => true})}; + {error, Error} -> + {200, [], rsp_map(#{success => false, + message => Error})} + end. + + %% ------------------------------------------------------------------ %% private API %% ------------------------------------------------------------------ @@ -107,4 +131,18 @@ thing_to_list(X) when is_list(X) -> X.
+%% Convert the given response map to a format acceptable by the erf +-spec rsp_map(map()) -> map(). +rsp_map(M) -> + Fun = fun(K, V) -> {bval(K), bval(V)} end, + maps:from_list([Fun(K, V) || {K, V} <- maps:to_list(M)]). + + +bval(V) when is_boolean(V) -> V; +bval(V) when is_atom(V) -> atom_to_binary(V); +bval(V) when is_list(V) -> list_to_binary(V); +bval(V) when is_map(V) -> rsp_map(V); +bval(V) -> V. + + %% vim:set ts=4 sw=4 et: