fixeria submitted this change.

View Change



5 is the latest approved patch-set.
No files were changed between the latest approved patch-set and the submitted one.

Approvals: pespin: Looks good to me, approved Jenkins Builder: Verified
[REST] Add MmeList, MmeAdd, MmeInfo, MmeDelete

Change-Id: Iad249aed99face9e35fd19e0596cf2364ade4c77
Related: SYS#7052
---
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, 578 insertions(+), 15 deletions(-)

diff --git a/contrib/openapi.yaml b/contrib/openapi.yaml
index 7f81608..6ab1346 100644
--- a/contrib/openapi.yaml
+++ b/contrib/openapi.yaml
@@ -66,6 +66,58 @@
'200':
$ref: '#/components/responses/OperationResult'

+ /mme-list:
+ get:
+ summary: Get a list of registered MMEs
+ operationId: MmeList
+ responses:
+ '200':
+ description: A list of registered MMEs
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/MmeList'
+ post:
+ summary: Add an MME to the pool
+ operationId: MmeAdd
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/MmeItem'
+ responses:
+ '201':
+ description: Successful outcome (MME added)
+ '409':
+ description: Unsuccessful outcome (MME already registered?)
+
+ /mme/{MmeId}:
+ get:
+ summary: Get information about a specific MME
+ operationId: MmeInfo
+ parameters:
+ - $ref: '#/components/parameters/MmeId'
+ responses:
+ '200':
+ description: Successful outcome (MME info)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/MmeItem'
+ '404':
+ description: Unsuccessful outcome (MME not found)
+ delete:
+ summary: Delete an MME from the pool
+ operationId: MmeDelete
+ parameters:
+ - $ref: '#/components/parameters/MmeId'
+ responses:
+ '200':
+ description: Successful outcome (MME deleted)
+ '404':
+ description: Unsuccessful outcome (MME not found)
+
/enb-list:
get:
summary: Get a list of eNB connections
@@ -168,6 +220,22 @@
$ref: '#/components/schemas/OperationResult'

parameters:
+ MmeId:
+ name: MmeId
+ in: path
+ description: MME identifier (selector)
+ required: true
+ schema:
+ oneOf:
+ - type: string
+ pattern: '^name:'
+ description: MME name
+ example: mme0
+ - type: string
+ pattern: '^addr:[0-9a-f:.]+-[0-9]+$'
+ description: MME remote address/port
+ example: addr:192.168.1.1-36412
+
EnbId:
name: EnbId
in: path
@@ -271,6 +339,38 @@
type: integer
description: Remote Recovery TimeStamp

+ MmeList:
+ type: array
+ items:
+ $ref: '#/components/schemas/MmeItem'
+
+ MmeItem:
+ type: object
+ required:
+ - name
+ - raddr
+ properties:
+ name:
+ type: string
+ description: Unique, human-readable identifier
+ laddr:
+ type: string
+ description: Local (bind) IP address
+ default: any
+ raddr:
+ type: string
+ description: Remote (connect) IP address
+ rport:
+ type: integer
+ description: Remote port
+ default: 36412
+ tac_list:
+ type: array
+ items:
+ type: integer
+ description: List of allowed TACs (empty means allow-all)
+ default: []
+
EnbList:
type: array
items:
diff --git a/contrib/osmo-s1gw-cli.py b/contrib/osmo-s1gw-cli.py
index b51a667..265c5fc 100755
--- a/contrib/osmo-s1gw-cli.py
+++ b/contrib/osmo-s1gw-cli.py
@@ -102,6 +102,26 @@
with self.send_post_req('pfcp/heartbeat') as f:
return json.load(f)

+ def mme_list(self) -> RESTResponse:
+ ''' MmeList :: Get a list of registered MMEs '''
+ with self.send_get_req('mme-list') as f:
+ return json.load(f)
+
+ def mme_add(self, mme_info: dict) -> int:
+ ''' MmeAdd :: Add an MME to the pool '''
+ with self.send_post_req('mme-list', mme_info) as f:
+ return f.status
+
+ def mme_info(self, name: str) -> RESTResponse:
+ ''' MmeInfo :: Get information about a specific MME '''
+ with self.send_get_req(f'mme/name:{name}') as f:
+ return json.load(f)
+
+ def mme_delete(self, name: str) -> int:
+ ''' MmeInfo :: Get information about a specific MME '''
+ with self.send_delete_req(f'mme/name:{name}') as f:
+ return f.status
+
def enb_list(self) -> RESTResponse:
''' EnbList :: Get a list of eNB connections '''
with self.send_get_req('enb-list') as f:
@@ -143,6 +163,7 @@

CAT_METRICS = 'Metrics commands'
CAT_PFCP = 'PFCP related commands'
+ CAT_MME = 'MME related commands'
CAT_ENB = 'eNB related commands'
CAT_ERAB = 'E-RAB related commands'

@@ -229,6 +250,115 @@
self.perror('Heartbeat failed: {message}'.format(**data))

@staticmethod
+ def add_sort_group(parser,
+ default: str,
+ choices: set[str]) -> None:
+ ''' Add argparse group with sorting params '''
+ sort_group = parser.add_argument_group('Sorting options')
+ sort_group.add_argument('-S', '--sort-by',
+ type=str,
+ default=default,
+ choices=choices,
+ help='Sort by (default: %(default)s)')
+ sort_group.add_argument('--reverse',
+ action='store_true',
+ help='Reverse order (default: %(default)s)')
+
+ @staticmethod
+ def mme_list_item(item: dict) -> dict:
+ ''' Generate a table row for the given MME '''
+ tac_list = item.get('tac_list')
+ tac_list = ', '.join(map(str, tac_list))
+ return {
+ 'Name': item.get('name'),
+ 'Local address': item.get('laddr'),
+ 'Remote address/port': '{raddr}:{rport}'.format(**item),
+ 'Allowed TACs': tac_list or 'all',
+ }
+
+ def mme_list_print(self, items: list[dict],
+ sort_by: str = 'none',
+ reverse: bool = False) -> None:
+ ''' Print a [sorted] list of MMEs in tabular form '''
+ if sort_by != 'none':
+ items.sort(key=lambda item: item.get(sort_by), reverse=reverse)
+ self.poutput(tabulate.tabulate(map(self.mme_list_item, items),
+ headers='keys', tablefmt=self.tablefmt))
+
+ def mme_info_print(self, item: dict) -> None:
+ ''' Print MME info in tabular form '''
+ self.poutput(tabulate.tabulate(self.mme_list_item(item).items(),
+ headers=['Parameter', 'Value'],
+ tablefmt=self.tablefmt))
+
+ mme_list_parser = cmd2.Cmd2ArgumentParser()
+ add_sort_group(mme_list_parser, default='none',
+ choices=('none', 'name', 'laddr', 'raddr'))
+
+ @cmd2.with_argparser(mme_list_parser)
+ @cmd2.with_category(CAT_MME)
+ def do_mme_list(self, opts) -> None:
+ ''' Get a list of registered MMEs '''
+ data = self.iface.mme_list()
+ self.mme_list_print(data, opts.sort_by, opts.reverse)
+
+ mme_add_parser = cmd2.Cmd2ArgumentParser()
+ mme_add_parser.add_argument('-N', '--name',
+ type=str, required=True,
+ help='MME name (example: mme0)')
+ mme_add_parser.add_argument('-la', '--laddr',
+ type=str, default='any',
+ help='Local address (default: %(default)s)')
+ mme_add_parser.add_argument('-ra', '--raddr',
+ type=str, required=True,
+ help='Remote address (example: 192.168.1.101)')
+ mme_add_parser.add_argument('-rp', '--rport',
+ type=int, default=36412,
+ help='Remote port (default: %(default)s)')
+ mme_add_parser.add_argument('--tac',
+ type=int, default=[],
+ action='append', dest='tac_list',
+ help='Allowed TAC (Tracking Area Code)')
+
+ @cmd2.with_argparser(mme_add_parser)
+ @cmd2.with_category(CAT_MME)
+ def do_mme_add(self, opts) -> None:
+ ''' Add an MME to the pool '''
+ mme_info = dict(name=opts.name,
+ laddr=opts.laddr,
+ raddr=opts.raddr,
+ rport=opts.rport,
+ tac_list=opts.tac_list)
+ self.iface.mme_add(mme_info)
+
+ @staticmethod
+ def add_mme_id_group(parser):
+ ''' Add argparse group for the MmeId parameter '''
+ mme_id_group = parser.add_argument_group('MME ID')
+ mme_id_group = mme_id_group.add_mutually_exclusive_group(required=True)
+ mme_id_group.add_argument('-N', '--name',
+ type=str,
+ help='MME name (example: mme0)')
+ # TODO: address/port
+ return mme_id_group
+
+ mme_info_parser = cmd2.Cmd2ArgumentParser()
+ add_mme_id_group(mme_info_parser)
+
+ @cmd2.with_argparser(mme_info_parser)
+ @cmd2.with_category(CAT_MME)
+ def do_mme_info(self, opts) -> None:
+ ''' Get information about a specific MME '''
+ data = self.iface.mme_info(opts.name)
+ self.mme_info_print(data)
+
+ @cmd2.with_argparser(mme_info_parser)
+ @cmd2.with_category(CAT_MME)
+ def do_mme_delete(self, opts) -> None:
+ ''' Delete an MME from the pool '''
+ self.iface.mme_delete(opts.name)
+
+ @staticmethod
def enb_list_item(item: dict) -> dict:
''' Generate a table row for the given eNB '''
enb_addr = lambda item: '{enb_saddr}:{enb_sport} ({enb_sctp_aid})'.format(**item)
@@ -258,21 +388,6 @@
headers=['Parameter', 'Value'],
tablefmt=self.tablefmt))

- @staticmethod
- def add_sort_group(parser,
- default: str,
- choices: set[str]) -> None:
- ''' Add argparse group with sorting params '''
- sort_group = parser.add_argument_group('Sorting options')
- sort_group.add_argument('-S', '--sort-by',
- type=str,
- default=default,
- choices=choices,
- help='Sort by (default: %(default)s)')
- sort_group.add_argument('--reverse',
- action='store_true',
- help='Reverse order (default: %(default)s)')
-
enb_list_parser = cmd2.Cmd2ArgumentParser()
add_sort_group(enb_list_parser, default='handle',
choices=('handle', 'pid', 'state', 'genb_id', 'uptime'))
diff --git a/doc/osmo-s1gw-cli.md b/doc/osmo-s1gw-cli.md
index be5ffc3..d50659e 100644
--- a/doc/osmo-s1gw-cli.md
+++ b/doc/osmo-s1gw-cli.md
@@ -151,6 +151,109 @@
Heartbeat failed: timeout
```

+### `mme_list`
+
+Get a list of registered MMEs.
+
+```
+Usage: mme_list [-h] [-S {none, name, laddr, raddr}] [--reverse]
+
+Get a list of registered MMEs
+
+optional arguments:
+ -h, --help show this help message and exit
+
+Sorting options:
+ -S, --sort-by {none, name, laddr, raddr}
+ Sort by (default: none)
+ --reverse Reverse order (default: False)
+```
+
+Example: getting a list of MMEs (not sorted by default).
+
+```
+OsmoS1GW# mme_list
+| Name | Local address | Remote address/port |
+|--------|-----------------|-----------------------|
+| mme0 | :: | 127.0.2.10:36412 |
+| mme1 | :: | 127.0.2.20:36412 |
+| mme2 | :: | 127.0.2.30:36412 |
+```
+
+### `mme_add`
+
+Add an MME to the pool.
+
+```
+Usage: mme_add -N NAME -ra RADDR [-h] [-la LADDR] [-rp RPORT]
+
+Add an MME to the pool
+
+required arguments:
+ -N, --name NAME MME name (example: mme0)
+ -ra, --raddr RADDR Remote address (example: 192.168.1.101)
+
+optional arguments:
+ -h, --help show this help message and exit
+ -la, --laddr LADDR Local address (default: ::)
+ -rp, --rport RPORT Remote port (default: 36412)
+```
+
+Example: adding an MME with remote address "192.168.1.101".
+
+```
+OsmoS1GW# mme_add --name mme42 --raddr 192.168.1.101
+```
+
+### `mme_info`
+
+Get information about a specific MME.
+
+```
+Usage: mme_info [-h] -N NAME
+
+Get information about a specific MME
+
+optional arguments:
+ -h, --help show this help message and exit
+
+MME ID:
+ -N, --name NAME MME name (example: mme0)
+```
+
+Example: getting information about an MME with name "mme0".
+
+```
+OsmoS1GW# mme_info --name mme0
+| Parameter | Value |
+|---------------------|------------------|
+| Name | mme0 |
+| Local address | :: |
+| Remote address/port | 127.0.2.10:36412 |
+```
+
+### `mme_delete`
+
+Delete an MME from the pool.
+
+```
+Usage: mme_delete [-h] -N NAME
+
+Delete an MME from the pool
+
+optional arguments:
+ -h, --help show this help message and exit
+
+MME ID:
+ -N, --name NAME MME name (example: mme0)
+```
+
+Example: deleting an MME with name "mme0".
+
+```
+OsmoS1GW# mme_delete --name mme0
+```
+
### `enb_list`

Get a list of eNB connections.
diff --git a/priv/openapi.json b/priv/openapi.json
index 6bcefdb..87aafac 100644
--- a/priv/openapi.json
+++ b/priv/openapi.json
@@ -100,6 +100,89 @@
}
}
},
+ "/mme-list": {
+ "get": {
+ "summary": "Get a list of registered MMEs",
+ "operationId": "MmeList",
+ "responses": {
+ "200": {
+ "description": "A list of registered MMEs",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MmeList"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "Add an MME to the pool",
+ "operationId": "MmeAdd",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MmeItem"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Successful outcome (MME added)"
+ },
+ "409": {
+ "description": "Unsuccessful outcome (MME already registered?)"
+ }
+ }
+ }
+ },
+ "/mme/{MmeId}": {
+ "get": {
+ "summary": "Get information about a specific MME",
+ "operationId": "MmeInfo",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/MmeId"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful outcome (MME info)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MmeItem"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unsuccessful outcome (MME not found)"
+ }
+ }
+ },
+ "delete": {
+ "summary": "Delete an MME from the pool",
+ "operationId": "MmeDelete",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/MmeId"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful outcome (MME deleted)"
+ },
+ "404": {
+ "description": "Unsuccessful outcome (MME not found)"
+ }
+ }
+ }
+ },
"/enb-list": {
"get": {
"summary": "Get a list of eNB connections",
@@ -263,6 +346,28 @@
}
},
"parameters": {
+ "MmeId": {
+ "name": "MmeId",
+ "in": "path",
+ "description": "MME identifier (selector)",
+ "required": true,
+ "schema": {
+ "oneOf": [
+ {
+ "type": "string",
+ "pattern": "^name:",
+ "description": "MME name",
+ "example": "mme0"
+ },
+ {
+ "type": "string",
+ "pattern": "^addr:[0-9a-f:.]+-[0-9]+$",
+ "description": "MME remote address/port",
+ "example": "addr:192.168.1.1-36412"
+ }
+ ]
+ }
+ },
"EnbId": {
"name": "EnbId",
"in": "path",
@@ -406,6 +511,47 @@
}
}
},
+ "MmeList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MmeItem"
+ }
+ },
+ "MmeItem": {
+ "type": "object",
+ "required": [
+ "name",
+ "raddr"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Unique, human-readable identifier"
+ },
+ "laddr": {
+ "type": "string",
+ "description": "Local (bind) IP address",
+ "default": "any"
+ },
+ "raddr": {
+ "type": "string",
+ "description": "Remote (connect) IP address"
+ },
+ "rport": {
+ "type": "integer",
+ "description": "Remote port",
+ "default": 36412
+ },
+ "tac_list": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "List of allowed TACs (empty means allow-all)",
+ "default": []
+ }
+ }
+ },
"EnbList": {
"type": "array",
"items": {
diff --git a/src/rest_server.erl b/src/rest_server.erl
index 9535939..79090e8 100644
--- a/src/rest_server.erl
+++ b/src/rest_server.erl
@@ -37,6 +37,10 @@
-export([metrics_list/1,
pfcp_assoc_state/1,
pfcp_heartbeat/1,
+ mme_list/1,
+ mme_add/1,
+ mme_info/1,
+ mme_delete/1,
enb_list/1,
enb_info/1,
enb_delete/1,
@@ -95,6 +99,46 @@
end.


+%% MmeList :: Get a list of registered MMEs
+mme_list(#{}) ->
+ MmeList = mme_registry:fetch_mme_list(),
+ {200, [], lists:map(fun mme_item_build/1, MmeList)}.
+
+
+%% MmeAdd :: Add an MME to the pool
+mme_add(#{body := Body}) ->
+ MmeInfo = mme_item_parse(Body),
+ case mme_registry:mme_register(MmeInfo) of
+ ok -> {201, [], undefined};
+ {error, _} -> {409, [], undefined}
+ end.
+
+
+%% MmeInfo :: Get information about a specific MME
+mme_info(#{path_parameters := PP}) ->
+ [{<< "MmeId" >>, << ID/bytes >>}] = PP,
+ case fetch_mme_info(ID) of
+ {ok, MmeInfo} ->
+ {200, [], mme_item_build(MmeInfo)};
+ error ->
+ {404, [], undefined}
+ end.
+
+
+%% MmeDelete :: Delete an MME from the pool
+mme_delete(#{path_parameters := PP}) ->
+ [{<< "MmeId" >>, << ID/bytes >>}] = PP,
+ case fetch_mme_info(ID) of
+ {ok, #{name := MmeName}} ->
+ case mme_registry:mme_unregister(MmeName) of
+ ok -> {200, [], undefined};
+ {error, _} -> {404, [], undefined}
+ end;
+ error ->
+ {404, [], undefined}
+ end.
+
+
%% EnbList :: Get a list of eNB connections
enb_list(#{}) ->
EnbList = enb_registry:fetch_enb_list(),
@@ -220,6 +264,60 @@
thing_to_list(X) when is_list(X) -> X.


+%% Parse an MmeItem object (received from user)
+-spec mme_item_parse(map()) -> mme_registry:mme_info().
+mme_item_parse(MmeItem) ->
+ maps:fold(fun mme_item_parse_field/3, #{ }, MmeItem).
+
+
+%% Parse MmeItem object fields (received from user)
+mme_item_parse_field(<< "name" >>, Name, MmeInfo) ->
+ MmeInfo#{name => binary_to_list(Name)};
+mme_item_parse_field(<< "laddr" >>, << "any" >>, MmeInfo) ->
+ MmeInfo#{laddr => any};
+mme_item_parse_field(<< "laddr" >>, LAddr, MmeInfo) ->
+ MmeInfo#{laddr => binary_to_list(LAddr)};
+mme_item_parse_field(<< "raddr" >>, RAddr, MmeInfo) ->
+ MmeInfo#{raddr => binary_to_list(RAddr)};
+mme_item_parse_field(<< "rport" >>, RPort, MmeInfo) ->
+ MmeInfo#{rport => RPort};
+mme_item_parse_field(<< "tac_list" >>, TACs, MmeInfo) ->
+ MmeInfo#{tac_list => TACs};
+mme_item_parse_field(_K, _V, MmeInfo) -> MmeInfo.
+
+
+%% Build an MmeItem object (sent to user)
+-spec mme_item_build(mme_registry:mme_info()) -> map().
+mme_item_build(MmeInfo) ->
+ maps:fold(fun mme_item_build_field/3, #{ }, MmeInfo).
+
+%% Build MmeItem object fields (sent to user)
+mme_item_build_field(name, Name, MmeItem) ->
+ MmeItem#{<< "name" >> => bval(Name)};
+mme_item_build_field(laddr, any, MmeItem) ->
+ MmeItem#{<< "laddr" >> => << "any" >>};
+mme_item_build_field(laddr, LAddr, MmeItem) ->
+ MmeItem#{<< "laddr" >> => bval(inet:ntoa(LAddr))};
+mme_item_build_field(raddr, RAddr, MmeItem) ->
+ MmeItem#{<< "raddr" >> => bval(inet:ntoa(RAddr))};
+mme_item_build_field(rport, RPort, MmeItem) ->
+ MmeItem#{<< "rport" >> => RPort};
+mme_item_build_field(tac_list, TACs, MmeItem) ->
+ MmeItem#{<< "tac_list" >> => TACs};
+mme_item_build_field(_K, _V, MmeItem) -> MmeItem.
+
+
+-spec fetch_mme_info(binary()) -> {ok, mme_registry:mme_info()} | error.
+fetch_mme_info(<< "name:", Val/bytes >>) ->
+ MmeName = binary_to_list(Val),
+ mme_registry:fetch_mme_info(MmeName);
+
+%% TODO: '^addr:[0-9a-f:.]+-[0-9]+$'
+fetch_mme_info(ID) ->
+ ?LOG_ERROR("Unhandled MME ID ~p", [ID]),
+ error.
+
+
-spec enb_item(enb_registry:enb_info()) -> map().
enb_item(EnbInfo) ->
M0 = #{handle => maps:get(handle, EnbInfo),
@@ -395,6 +493,7 @@
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({?FUNCTION_NAME, V}) -> V;
bval(V) -> V.



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

Gerrit-MessageType: merged
Gerrit-Project: erlang/osmo-s1gw
Gerrit-Branch: master
Gerrit-Change-Id: Iad249aed99face9e35fd19e0596cf2364ade4c77
Gerrit-Change-Number: 41282
Gerrit-PatchSet: 7
Gerrit-Owner: fixeria <vyanitskiy@sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: fixeria <vyanitskiy@sysmocom.de>
Gerrit-Reviewer: osmith <osmith@sysmocom.de>
Gerrit-Reviewer: pespin <pespin@sysmocom.de>