From 4f3785a1cab7e35a9fb0e6aa8ce95e07a2c68a16 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 18 Sep 2023 17:55:38 +0200 Subject: [PATCH 1/5] ns-api: add validation for ns.dhcp --- packages/ns-api/files/ns.dhcp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/ns-api/files/ns.dhcp b/packages/ns-api/files/ns.dhcp index b91c88d10..830d495d1 100755 --- a/packages/ns-api/files/ns.dhcp +++ b/packages/ns-api/files/ns.dhcp @@ -94,7 +94,7 @@ def edit_interface(args): try: u.get_all("network", args["interface"]) except: - return {"error": "interface not found"} + return utils.generic_error("interface_not_found") u.set("dhcp", args['interface'], 'dhcp') ipaddr = u.get('network', args['interface'], 'ipaddr') netmask = u.get('network', args['interface'], 'netmask') @@ -124,7 +124,7 @@ def get_interface(args): dhcp = u.get_all("dhcp", args["interface"]) interface = u.get_all("network", args["interface"]) except: - return {"error": "interface not found"} + return utils.generic_error("interface not found") (ret['first'], ret['last']) = conf_to_range(interface['ipaddr'], interface['netmask'], dhcp.get('start', 100), dhcp.get('limit', 150)) ret["leasetime"] = dhcp["leasetime"] ret["active"] = dhcp["dhcpv4"] == "server" @@ -166,8 +166,21 @@ def list_static_leases(): ret['leases'].append({"lease": l, "macaddr": ldata['mac'], "ipaddr": ldata['ip'], "hostname": ldata.get('name',''), 'interface': interface, "device": device, "description": ldata.get('ns_description', '')}) return ret +def is_reserved(u, mac): + try: + for h in utils.get_all_by_type(u, 'dhcp', 'host'): + if u.get('dhcp', h, 'mac', default='') == mac: + return True + except: + return False + + return False + + def add_static_lease(args): u = EUci() + if is_reserved(u, args["macaddr"]): + return utils.validation_error("mac", "mac_already_reserved", args["macaddr"]) lname = utils.get_random_id() u.set('dhcp', lname, 'host') u.set('dhcp', lname, 'ip', args["ipaddr"]) @@ -184,7 +197,10 @@ def edit_static_lease(args): try: u.get('dhcp', args['lease']) except: - return {"error": "lease not found"} + return utils.generic_error("lease_not_found") + + if is_reserved(args["macaddr"]): + return utils.validation_error("mac", "mac_already_reserved", args["macaddr"]) lname = args["lease"] u.set('dhcp', lname, 'host') u.set('dhcp', lname, 'ip', args["ipaddr"]) @@ -203,7 +219,7 @@ def get_static_lease(args): try: lease = u.get_all('dhcp', args['lease']) except: - return {"error": "lease not found"} + return utils.generic_error("lease_not_found") return {"hostname": lease.get('name', ''), "ipaddr": lease.get("ip", ''), 'macaddr': lease.get('mac',''), 'description': lease.get('ns_description', '')} def delete_static_lease(args): @@ -213,7 +229,7 @@ def delete_static_lease(args): u.delete('dhcp', args['lease']) u.save('dhcp') except: - return {"error": "lease not found"} + return utils.generic_error("lease_not_found") return {"lease": args['lease']} cmd = sys.argv[1] @@ -232,6 +248,7 @@ if cmd == 'list': {"120": "192.168.100.151"} ]}, "list-active-leases": {}, + "list-static-leases": {}, "get-static-lease": {"lease": "ns_mylease"}, "edit-static-lease": {"lease": "ns_mylease"}, "delete-static-lease": {"lease": "ns_mylease"}, From a0db1c52a925b473a7f9fabaf5580c7d24f0c055 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 19 Sep 2023 08:35:00 +0200 Subject: [PATCH 2/5] python3-nethsec: bump version Import error functions --- packages/python3-nethsec/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/python3-nethsec/Makefile b/packages/python3-nethsec/Makefile index 932576ef9..8f1b178bc 100644 --- a/packages/python3-nethsec/Makefile +++ b/packages/python3-nethsec/Makefile @@ -6,15 +6,15 @@ include $(TOPDIR)/rules.mk PKG_NAME:=python3-nethsec -PKG_VERSION:=0.0.1 +PKG_VERSION:=0.0.2 PKG_RELEASE:=1 PKG_MAINTAINER:=Giacomo Sanchietti PKG_LICENSE:=GPL-3.0-only -PKG_SOURCE_PROTO:=git -PKG_SOURCE_URL:=https://github.com/NethServer/python3-nethsec -PKG_SOURCE_VERSION:=74d57b17f6e87365a294a83d5cc49288efbc0caf +PKG_SOURCE:=python3-nethsec-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://codeload.github.com/nethserver/python3-nethsec/tar.gz/$(PKG_VERSION)? +PKG_HASH:=skip include $(INCLUDE_DIR)/package.mk include $(TOPDIR)/feeds/packages/lang/python/python3-package.mk From 4832d049ae54b962bb0ab3fe325cb6f03a70f6a7 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 19 Sep 2023 09:16:11 +0200 Subject: [PATCH 3/5] ns-api: use errors functions from python3-nethsec --- packages/ns-api/README.md | 49 ++++++++++++++++++--------- packages/ns-api/files/ns.dns | 10 +++--- packages/ns-api/files/ns.power | 3 +- packages/ns-api/files/ns.routes | 12 +++---- packages/ns-api/files/ns.subscription | 9 ++--- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index dba1dfb24..75333f4e7 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -715,7 +715,7 @@ Success response example: Error response example: ```json -{ "error": "command failed" } +{ "error": "command_failed" } ``` ### poweroff @@ -732,7 +732,7 @@ Success response example: Error response example: ```json -{ "error": "command failed" } +{ "error": "command_failed" } ``` ## ns.routes @@ -938,7 +938,7 @@ Success response example, the id of the edited route: Error response example: ```json -{"error": "route not modified"} +{"error": "route_not_modified"} ``` ### delete-route @@ -957,7 +957,7 @@ Success response example, the id of the deleted route: Error response example: ```json -{"error": "route not deleted"} +{"error": "route_not_deleted"} ``` ### enable-route @@ -976,7 +976,7 @@ Success response example, the id of the enabled route: Error response example: ```json -{"error": "route not enabled"} +{"error": "route_not_enabled"} ``` ### disable-route @@ -995,7 +995,7 @@ Success response example, the id of the disabled route: Error response example: ```json -{"error": "route not disabled"} +{"error": "route_not_disabled"} ``` ## ns.dashboard @@ -1168,7 +1168,7 @@ Success response: Error response: ```json -{"error": "invalid secret or server not found"} +{"error": "invalid_secret_or_server_not_found"} ``` ### unregister @@ -1185,7 +1185,7 @@ Success response: Error response: ```json -{"error": "unregister failure"} +{"error": "unregister_failure"} ``` ### info @@ -1210,7 +1210,7 @@ If the subscription does not expire, `expiration` is set to `0`. Error response: ```json -{"error": "no subscription info found"} +{"error": "no_subscription_info_found"} ``` ## ns.dhcp @@ -1314,7 +1314,7 @@ Multiple values can be comma-separated. Error response example: ```json -{"error": "interface not found"} +{"error": "interface_not_found"} ``` ### edit-interface @@ -1333,7 +1333,7 @@ Successfull response: Error response example: ```json -{"error": "interface not found"} +{"error": "interface_not_found"} ``` ### list-active-leases @@ -1418,7 +1418,7 @@ Successfull response example: Error response example: ```json -{"error": "lease not found"} +{"error": "lease_not_found"} ``` ### delete-static-lease @@ -1435,7 +1435,7 @@ Successfull response example: Error response example: ```json -{"error": "lease not found"} +{"error": "lease_not_found"} ``` ### add-static-lease @@ -1450,6 +1450,21 @@ Successfull response example: {"lease": "ns_d5facd89"} ``` +If the mac address has already a reservation, a validation error is returned: +``` +{ + "validation": { + "errors": [ + { + "parameter": "mac", + "message": "mac_already_reserved", + "value": "80:5e:c0:d9:c6:9b" + } + ] + } +} +``` + ### edit-static-lease Edit static lease: @@ -1464,9 +1479,11 @@ Successfull response example: Error response example: ```json -{"error": "lease not found"} +{"error": "lease_not_found"} ``` +This API can also raise a validation error like the `add-static-lease` API. + ## ns.dns Manage global DNS configuration and DNS records. @@ -1538,7 +1555,7 @@ Successful response example: Error response example: ```json -{"error": "record not found"} +{"error": "record_not_found"} ``` ### delete-record @@ -1555,7 +1572,7 @@ Successful response example: Error response example: ```json -{"error": "record not found"} +{"error": "record_not_found"} ``` ### get-config diff --git a/packages/ns-api/files/ns.dns b/packages/ns-api/files/ns.dns index cb2a83281..fdf38c1aa 100755 --- a/packages/ns-api/files/ns.dns +++ b/packages/ns-api/files/ns.dns @@ -75,7 +75,7 @@ def edit_record(args): try: u.get('dhcp', args['record']) except: - return {"error": "record not found"} + return utils.generic_error("record_not_found") rname = args["record"] u.set('dhcp', rname, 'domain') u.set('dhcp', rname, 'ip', args["ip"]) @@ -93,7 +93,7 @@ def get_record(args): try: record = u.get_all('dhcp', args['record']) except: - return {"error": "record not found"} + return utils.generic_error("record_not_found") return {"name": record.get('name', ''), "ip": record.get("ip", ''), "description": record.get("ns_description",""), 'wildcard': is_wildcard(u, record.get("ip", ''), record.get('name', ''))} def delete_record(args): @@ -103,7 +103,7 @@ def delete_record(args): u.delete('dhcp', args['record']) u.save('dhcp') except: - return {"error": "record not found"} + return utils.generic_error("record_not_found") return {"record": args['record']} def get_server(u): @@ -122,7 +122,7 @@ def get_config(): ret["logqueries"] = config.get("logqueries", "0") == "1" ret["server"] = config.get("server", []) except: - return {"error": "config read error"} + return utils.generic_error("config_read_error") return ret def bool2str(bval): @@ -147,7 +147,7 @@ def set_config(args): u.set("dhcp", section, 'local', f'/{args["domain"]}/') u.save("dhcp") except: - return {"error": "config write error"} + return utils.generic_error("config_write_error") return {"server": section} diff --git a/packages/ns-api/files/ns.power b/packages/ns-api/files/ns.power index ed4db61e6..52b2d593f 100755 --- a/packages/ns-api/files/ns.power +++ b/packages/ns-api/files/ns.power @@ -10,13 +10,14 @@ import sys import json import subprocess +from nethsec import utils def run(cmd): try: subprocess.run([cmd], check=True, capture_output=True) return {"result": "success"} except: - return {"error": "command failed"} + return utils.generic_error("command_failed") cmd = sys.argv[1] diff --git a/packages/ns-api/files/ns.routes b/packages/ns-api/files/ns.routes index f4c64b700..00fb6f79f 100755 --- a/packages/ns-api/files/ns.routes +++ b/packages/ns-api/files/ns.routes @@ -75,7 +75,7 @@ def enable_route(rid): u.save("network") return {"id": rid} except: - return {"error": "route not enabled"} + return utils.generic_error("route_not_enabled") def disable_route(rid): u = EUci() @@ -84,7 +84,7 @@ def disable_route(rid): u.save("network") return {"id": rid} except: - return {"error": "route not disabled"} + return utils.generic_error("route_not_disabled") def delete_route(rid): u = EUci() @@ -94,7 +94,7 @@ def delete_route(rid): u.save("network") return {"id": rid} except: - return {"error": "route not deleted"} + return utils.generic_error("route_not_deleted") def add_route(args): u = EUci() @@ -111,7 +111,7 @@ def add_route(args): u.save("network") return {"id": rname} except: - return {"error": "route not added"} + return utils.generic_error("route_not_added") def edit_route(args): u = EUci() @@ -122,7 +122,7 @@ def edit_route(args): u.save("network") return {"id": rname} except: - return {"error": "route not modified"} + return utils.generic_error("route_not_modified") def list_interfaces(): u = EUci() @@ -169,6 +169,6 @@ elif cmd == 'call': elif action == "disable-route": ret = disable_route(args["id"]) else: - ret = {"error": "invalid action"} + ret = utils.generic_error("invalid_action") print(json.dumps(ret)) diff --git a/packages/ns-api/files/ns.subscription b/packages/ns-api/files/ns.subscription index ea4b8b167..13780ed08 100755 --- a/packages/ns-api/files/ns.subscription +++ b/packages/ns-api/files/ns.subscription @@ -11,6 +11,7 @@ import sys import json import subprocess from datetime import datetime +from nethsec import utils def register(secret): try: @@ -23,14 +24,14 @@ def register(secret): subprocess.run(["/usr/sbin/register", "community", secret, '5'], check=True, capture_output=True) return {"result": "success"} except: - return {"error": "invalid secret or server not found"} + return utils.generic_error("invalid_secret_or_server_not_found") def unregister(): try: subprocess.run(["/usr/sbin/unregister"], check=True, capture_output=True) return {"result": "success"} except: - return {"error": "unregister failure"} + return utils.generic_error("unregister_failure") def info(): try: @@ -38,7 +39,7 @@ def info(): p = subprocess.run(["/usr/sbin/subscription-info"], check=True, capture_output=True) data = json.loads(p.stdout) except: - return {"error": "no subscription info found"} + return utils.generic_error("no_subscription_info_found") active = data["subscription"]["status"] == "valid" try: @@ -64,5 +65,5 @@ elif cmd == 'call': elif action == "info": ret = info() else: - ret = {"error": "invalid action"} + ret = utils.generic_error("invalid_action") print(json.dumps(ret)) From 5ed91e7c4760d76ca11f7029979c1ef05811f896 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 19 Sep 2023 09:48:59 +0200 Subject: [PATCH 4/5] doc: refactor API --- docs/api.md | 244 ++++++++++++++++++++++++++++++++++++++ packages/ns-api/README.md | 190 ----------------------------- 2 files changed, 244 insertions(+), 190 deletions(-) create mode 100644 docs/api.md diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 000000000..1aea83c86 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,244 @@ +--- +layout: default +title: APIs +nav_order: 20 +--- + +# APIs + +* TOC +{:toc} + +## Reference + +All APIs are documented inside the [ns-api](../packages/ns-api) package. + +## Usage + +APIs can be called using 4 different methods: +- using `curl` and invoking the API server +- using `api-cli` wrapper that talks with ubus over HTTP +- directly invoking the scripts +- using `ubus` client + +### curl + +The APIs should always be invoked through the API server on a production environment. +The server will handle the authentication and invoke APIs using ubus. +It also add some extra logic for 2FA and error handling. + +First, authenticate the user and obtain a JWT token: +``` +curl -s -H 'Content-Type: application/json' -k https://localhost/api/login --data '{"username": "root", "password": "Nethesis,1234"}' | jq -r .token +``` + +Use the obtained token, to invoke an API: +``` +curl -s -H 'Content-Type: application/json' -k https://localhost/api/ubus/call -H 'Authorization: Bearer ' --data '{"path": "ns.dashboard", "method": "system-info", "payload": {}}' | jq +``` + +If you need to pass parameters to the API, add them inside the `payload` field: +``` +curl -s -H 'Content-Type: application/json' -k https://localhost/api/ubus/call -H 'Authorization: Bearer ' --data '{"path": "ns.dashboard", "method": "counter", "payload": {"service": "hosts"}}' +``` + +### api-cli + +The `api-cli` wrapper needs valid user credentials. +If no credentials are given, it uses the default ones: + +- user: `root` +- password: `Nethesis,1234` + +You can use this method to automate the configuration and to test existing APIs. + +Example with default credentials: +``` +api-cli ns.dashboard system-info +``` + +If you changed the `root` password, use: +``` +api-cli --password mypass ns.dashboard system-info +``` + +You can pass parameters to the APIs: +## Conventions + +APIs are invoked using the [api-server](../packages/ns-api-server). +The server uses ubus to call the scripts that actually implement the real APIs. +Each script must be executable and follow [RPCD rules](https://openwrt.org/docs/techref/rpcd). +See also [JSONRPC](https://github.com/openwrt/luci/wiki/JsonRpcHowTo) for more info. + +Each API script should also respect the following conventions: +- all APIs must start with `ns.` prefix +- all APIs must read JSON object input from standard input +- all APIs must write a JSON object to standard output: JSON arrays should always be wrapped inside + an object due to ubus limitations +- APIs should not commit changes to the configuration database: it's up to the user (or the UI) to commit them and restart the services +- all APIs must follow the error and validation protocols described below + +### Error protocol + +If the API raises and error, it should return an object like: +```json +{ + "error": "command_failed" +} +``` + +The `error` field should contain an error message which can be translated inside the UI. +As general rule, the message: + +- should describe the error +- should be written in lower case +- should not contain special characters nor spaces + +The API server will parse the error and return a 500 HTTP status code and a full response like: +```json +{ + "code": 500, + "data": { + "error": "command_failed" + }, + "message": "command_failed" +} +``` + +If the API server can't catch the exact error, or the called method does not exists, the API server will return a response like: +```json +{ + "code": 500, + "data": "exit status 3", + "message": "ubus call action failed" +} +``` + +### Validation protocol + +The APIs should validate all data that can lead to a service failure. +If the validation fails, it should return an object like: +```json +{ + "validation": { + "errors": [ + { + "parameter": "mac", + "message": "mac_already_reserved", + "value": "80:5e:c0:d9:c6:9b" + } + ] + } +} +``` + +The `errors` fields contains all validation errors. +Fields of the error: +- `parameter`: name of the parameter that threw the error +- `message`: a valuable description of the error, see `error` field above for how to format the message +- `value`: the value that raised the error (optional) + +The API server will catch the validation and return a 400 HTTP code with a response like: +```json +{ + "code": 400, + "data": { + "validation": { + "errors": [ + { + "message": "mac_already_reserved", + "parameter": "mac", + "value": "80:5e:c0:d9:c6:9b" + } + ] + } + }, + "message": "validation_failed" +} +``` + +## Create a new API + +To add a new API, follow these steps: +1. create an executable file inside `/usr/libexec/rpcd/` directory, like `ns.example`, and restart RPCD +2. create an ACL JSON file inside `/usr/share/rpcd/acl.d`, like `ns.example.json` + +APIs can be written using any available programming language. +In this example we are going to use python. + +Example for `/usr/libexec/rpcd/ns.example`: +```python +#!/usr/bin/python3 + +# +# Copyright (C) 2023 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-2.0-only +# + +# Return the same message given as input parameter + +import sys +import json +from nethsec import utils + +def say(param): + res = '' + try: + # do something here + ret = param + finally: + print(json.dumps({"result": ret})) + +cmd = sys.argv[1] + +if cmd == 'list': + print(json.dumps({"say": {"message": "value"}})) +elif cmd == 'call' and sys.argv[2] == "say": + args = json.loads(sys.stdin.read()) + param = args.get('message', 'myvalue') + print(param) +else: + print(json.dumps(utils.generic_error("invalid_action"))) +``` + +Make the file executable and restart rpcd: +``` +chmod a+x /usr/libexec/rpcd/ns.example +``` + +Usage example: +``` +# ubus -v call ns.example say '{"message": "hello world"}' | jq +{ + "result": "hello world" +} +``` + +Create the ACL file to enable RPC calls. +Example for `/usr/share/rpcd/acl.d/ns.example.json`: +```json +{ + "example-reader": { + "description": "Access say method", + "write": {}, + "read": { + "ubus": { + "ns.example": [ + "say" + ] + } + } + } +} +``` + +Restart `rpcd`: +``` +/etc/init.d/rpcd restart +``` + +Test the new API: +``` +api-cli ns.example say --data '{"message": "hello world"}' +``` + diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 75333f4e7..26720563d 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -5,103 +5,6 @@ NethSecurity APIs for `rpcd`. * TOC {:toc} -## How to invoke the APIs - -APIs can be called using 4 different methods: -- using `curl` and invoking the API server -- using `api-cli` wrapper that talks with ubus over HTTP -- directly invoking the scripts -- using `ubus` client - -### curl - -The APIs should always be invoked through the API server on a production environment. -The server will handle the authentication and invoke APIs using ubus. -It also add some extra logic for 2FA and error handling. - -First, authenticate the user and obtain a JWT token: -``` -curl -s -H 'Content-Type: application/json' -k https://localhost/api/login --data '{"username": "root", "password": "Nethesis,1234"}' | jq -r .token -``` - -Use the obtained token, to invoke an API: -``` -curl -s -H 'Content-Type: application/json' -k https://localhost/api/ubus/call -H 'Authorization: Bearer ' --data '{"path": "ns.dashboard", "method": "system-info", "payload": {}}' | jq -``` - -If you need to pass parameters to the API, add them inside the `payload` field: -``` -curl -s -H 'Content-Type: application/json' -k https://localhost/api/ubus/call -H 'Authorization: Bearer ' --data '{"path": "ns.dashboard", "method": "counter", "payload": {"service": "hosts"}}' -``` - -### api-cli - -The `api-cli` wrapper needs valid user credentials. -If no credentials are given, it uses the default ones: - -- user: `root` -- password: `Nethesis,1234` - -You can use this method to automate the configuration and to test existing APIs. - -Example with default credentials: -``` -api-cli ns.dashboard system-info -``` - -If you changed the `root` password, use: -``` -api-cli --password mypass ns.dashboard system-info -``` - -You can pass parameters to the APIs: -``` -api-cli ns.dashboard counter --data '{"service": "hosts"}' -``` - -### Direct invocation - -Most APIs are implemented as executable scripts under the `/usr/libexec/rpcd` directory. -See also [Creating a new API](#creating_a_new_api) for more info. - -You can call directly the script bypassing all system checks. -Use this method during API development. - -Call the method `system-info` under the `ns.dashboard` API: -``` -/usr/libexec/rpcd/ns.dashboard call system-info | jq -``` - -If you need to pass an argument, you must write it to the standard input: -``` -echo '{"service": "hosts"}' | /usr/libexec/rpcd/ns.dashboard call counter | jq -``` - -To list all methods of a script use: -``` -/usr/libexec/rpcd/ns.dashboard list | jq -``` - -### ubus - -You can check if an API follows all conventions using ubus. -Even this method is recommended only during development. - -List available APIs: -``` -ubus -S list | grep '^ns.' -``` - -APIs can be called directly from ubus, like: -``` -ubus -S call ns.talkers list '{"limit": 5}' -``` -Or using `api-cli`: -``` -api-cli ns.talkers list --data '{"limit": 5}' -``` - - ## ns.talkers ### list @@ -1605,96 +1508,3 @@ Response example: {"server": "cfg01411c"} ``` -# Creating a new API - -Conventions: -- all APIs must start with `ns.` prefix -- all APIs must read JSON object input from standard input -- all APIs must write a JSON object to standard output: JSON arrays should always be wrapped inside - an object due to ubus limitations -- APIs should not commit changes to the configuration database: it's up to the user (or the UI) to commit them and restart the services -- if the API raises and error, it should return an object like `{"error": "my error"}` - -To add a new API, follow these steps: -1. create an executable file inside `/usr/libexec/rpcd/` directory, like `ns.example`, and restart rpcd -2. create an ACL JSON file inside `/usr/share/rpcd/acl.d`, like `ns.example.json` - -APIs can be written using any available programming language. -In this example we are going to use python. - -Example for `/usr/libexec/rpcd/ns.example`: -```python -#!/usr/bin/python3 - -# -# Copyright (C) 2022 Nethesis S.r.l. -# SPDX-License-Identifier: GPL-2.0-only -# - -# Return the same message given as input parameter - -import sys -import json - -def say(param): - res = '' - try: - # do something here - ret = param - finally: - print(json.dumps({"result": ret})) - -cmd = sys.argv[1] - -if cmd == 'list': - print(json.dumps({"say": {"message": "value"}})) -elif cmd == 'call' and sys.argv[2] == "say": - args = json.loads(sys.stdin.read()) - param = args.get('message', 'myvalue') - say(param) -``` - -Make the file executable and restart rpcd: -``` -chmod a+x /usr/libexec/rpcd/ns.example -``` - -Usage example: -``` -# ubus -v call ns.example say '{"message": "hello world"}' | jq -{ - "result": "hello world" -} -``` - -Create the ACL file to enable RPC calls. -Example for `/usr/share/rpcd/acl.d/ns.example.json`: -``` -{ - "example-reader": { - "description": "Access say method", - "write": {}, - "read": { - "ubus": { - "ns.example": [ - "say" - ] - } - } - } -} -``` - -Restart `rpcd`: -``` -/etc/init.d/rpcd restart -``` - -Test the new API: -``` -api-cli ns.example say --data '{"message": "hello world"}' -``` - -References -- [RPCD](https://openwrt.org/docs/techref/rpcd) -- [JSONRPC](https://github.com/openwrt/luci/wiki/JsonRpcHowTo) From 5623c6592804c9a977b7b82df381d43094b27275 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 19 Sep 2023 10:58:27 +0200 Subject: [PATCH 5/5] ns-api: apply suggestions from code review Co-authored-by: Andrea Leardini --- docs/api.md | 2 +- packages/ns-api/files/ns.dhcp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 1aea83c86..41f2aaf1c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -80,7 +80,7 @@ Each API script should also respect the following conventions: ### Error protocol -If the API raises and error, it should return an object like: +If the API raises an error, it should return an object like: ```json { "error": "command_failed" diff --git a/packages/ns-api/files/ns.dhcp b/packages/ns-api/files/ns.dhcp index 830d495d1..2d61cf22b 100755 --- a/packages/ns-api/files/ns.dhcp +++ b/packages/ns-api/files/ns.dhcp @@ -124,7 +124,7 @@ def get_interface(args): dhcp = u.get_all("dhcp", args["interface"]) interface = u.get_all("network", args["interface"]) except: - return utils.generic_error("interface not found") + return utils.generic_error("interface_not_found") (ret['first'], ret['last']) = conf_to_range(interface['ipaddr'], interface['netmask'], dhcp.get('start', 100), dhcp.get('limit', 150)) ret["leasetime"] = dhcp["leasetime"] ret["active"] = dhcp["dhcpv4"] == "server"