fixeria has uploaded this change for review.
[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, 518 insertions(+), 15 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/erlang/osmo-s1gw refs/changes/82/41282/1
diff --git a/contrib/openapi.yaml b/contrib/openapi.yaml
index a56f876..b7e07ec 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,30 @@
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
+ raddr:
+ type: string
+ description: Remote (connect) IP address
+ rport:
+ type: integer
+ description: Remote port
+
EnbList:
type: array
items:
diff --git a/contrib/osmo-s1gw-cli.py b/contrib/osmo-s1gw-cli.py
index b51a667..2303a4a 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,107 @@
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 '''
+ return {
+ 'Name': item.get('name'),
+ 'Local address': item.get('laddr'),
+ 'Remote address/port': '{raddr}:{rport}'.format(**item),
+ }
+
+ 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='::',
+ 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)')
+
+ @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)
+ 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 +380,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 4a0bd22..41114d5 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 5422c03..262ad4d 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,37 @@
}
}
},
+ "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"
+ },
+ "raddr": {
+ "type": "string",
+ "description": "Remote (connect) IP address"
+ },
+ "rport": {
+ "type": "integer",
+ "description": "Remote port"
+ }
+ }
+ },
"EnbList": {
"type": "array",
"items": {
diff --git a/src/rest_server.erl b/src/rest_server.erl
index 6e1f892..a69c73c 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,50 @@
end.
+%% MmeList :: Get a list of registered MMEs
+mme_list(#{}) ->
+ MmeList = mme_registry:fetch_mme_list(),
+ {200, [], lists:map(fun mme_item/1, MmeList)}.
+
+
+%% MmeAdd :: Add an MME to the pool
+mme_add(#{body := Body}) ->
+ MmeInfo = #{name => binary_to_list(maps:get(<< "name" >>, Body)),
+ laddr => binary_to_list(maps:get(<< "laddr" >>, Body, "::")),
+ raddr => binary_to_list(maps:get(<< "raddr" >>, Body)),
+ rport => maps:get(<< "rport" >>, Body, 36412)
+ },
+ 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(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 +268,23 @@
thing_to_list(X) when is_list(X) -> X.
+-spec mme_item(mme_registry:mme_info()) -> map().
+mme_item(MmeInfo) ->
+ rsp_map(MmeInfo#{laddr => inet:ntoa(maps:get(laddr, MmeInfo)),
+ raddr => inet:ntoa(maps:get(raddr, MmeInfo))}).
+
+
+-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),
To view, visit change 41282. To unsubscribe, or for help writing mail filters, visit settings.