Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/design/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ 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 <jwt_token>' --data '{"path": "ns.dashboard", "method": "counter", "payload": {"service": "hosts"}}'
```

To retrieve the initial standalone dashboard data with one API call:
```
curl -s -H 'Content-Type: application/json' -k https://localhost/api/ubus/call -H 'Authorization: Bearer <jwt_token>' --data '{"path": "ns.dashboard", "method": "summary", "payload": {}}' | jq
```

### api-cli

The `api-cli` wrapper needs valid user credentials.
Expand Down Expand Up @@ -74,6 +79,11 @@ You can pass parameters to the APIs:
/usr/bin/api-cli ns.dashboard counter --data '{"service": "hosts"}'
```

Or fetch the initial standalone dashboard payload:
```
api-cli ns.dashboard summary
```

Example of bash script:
```bash
hosts=$(echo '{"service": "hosts"}' | /usr/bin/api-cli ns.dashboard counter --data - | jq .result.count)
Expand Down Expand Up @@ -258,4 +268,3 @@ Test the new API:
```
api-cli ns.example say --data '{"message": "hello world"}'
```

71 changes: 71 additions & 0 deletions packages/ns-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2552,6 +2552,77 @@ Response example:
}
```

### summary

Return the initial standalone dashboard data in a single response:
```
api-cli ns.dashboard summary
```

Response example:
```json
{
"result": {
"systemInfo": {
"uptime": 8500.37,
"load": [0.0205078125, 0.00927734375, 0],
"version": {
"arch": "x86_64",
"release": "NethSecurity 22.03.5"
},
"hostname": "NethSec",
"hardware": "Standard PC (Q35 + ICH9, 2009)",
"memory": {
"mem_total": 939032576,
"mem_available": 860221440
},
"storage": {
"/": {
"used_bytes": 175554560,
"available_bytes": 127582208
},
"/mnt/data": {
"used_bytes": 0,
"available_bytes": 0
},
"tmpfs": {
"used_bytes": 14372864,
"available_bytes": 502484992
}
}
},
"serviceStatus": {
"internet": "ok",
"dns-configured": "ok",
"mwan": "disabled",
"netifyd": "ok",
"openvpn_rw": "ok",
"banip": "ok",
"threat_shield_dns": "disabled",
"dedalo": "disabled"
},
"counters": {
"hosts": 14,
"openvpn_rw": 2,
"threat_shield_ip": 8
},
"tunnels": {
"ipsec": {
"enabled": 2,
"connected": 1
},
"ovpn": {
"enabled": 1,
"connected": 1
}
},
"threatShield": {
"monitoringEnabled": true
}
}
}
```

### traffic-interface

Return an array of points describing the network traffic in the last hour.
Expand Down
88 changes: 63 additions & 25 deletions packages/ns-api/files/ns.dashboard
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ import urllib.request
from euci import EUci
from nethsec import utils, ovpn

SUMMARY_SERVICES = [
"internet",
"dns-configured",
"mwan",
"netifyd",
"openvpn_rw",
"banip",
"threat_shield_dns",
"dedalo",
]

SUMMARY_COUNTERS = ["hosts", "openvpn_rw", "threat_shield_ip"]

def get_uptime():
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.readline().split()[0])
Expand Down Expand Up @@ -138,6 +151,14 @@ def count_hosts():
def count_openvpn_rw():
return len(ovpn.list_connected_clients('ns_roadwarrior1').keys())


def is_threat_shield_monitoring_enabled():
u = EUci()
for option in ["ban_logforwardlan", "ban_logforwardwan", "ban_loginput", "ban_logprerouting"]:
if u.get("banip", "global", option, default="0") == "1":
return True
return False

def check_ts_dns():
u = EUci()
adb_status = check_adblock()
Expand Down Expand Up @@ -389,28 +410,45 @@ def list_wans():

return ret

cmd = sys.argv[1]

if cmd == 'list':
print(json.dumps({"system-info": {}, "list-wans":{}, "service-status": {"service": "myservice"}, "counter": {"service": "hosts"}, "interface-traffic": {"interface": "eth0"}, "ipsec-tunnels": {}, "ovpn-tunnels": {}}))
elif cmd == 'call':
action = sys.argv[2]
if action == "system-info":
ret = system_info()
if action == "list-wans":
ret = list_wans()
elif action == "service-status":
args = json.loads(sys.stdin.read())
ret = service_status(args["service"])
elif action == "counter":
args = json.loads(sys.stdin.read())
ret = counter(args["service"])
elif action == "interface-traffic":
args = json.loads(sys.stdin.read())
ret = interface_traffic(args["interface"])
elif action == "ipsec-tunnels":
ret = ipsec_tunnels()
elif action == "ovpn-tunnels":
ret = ovpn_tunnels()

print(json.dumps({"result": ret}))

def dashboard_summary():
return {
"systemInfo": system_info(),
"serviceStatus": {service: service_status(service)["status"] for service in SUMMARY_SERVICES},
"counters": {service: counter(service)["count"] for service in SUMMARY_COUNTERS},
"tunnels": {"ipsec": ipsec_tunnels(), "ovpn": ovpn_tunnels()},
"threatShield": {"monitoringEnabled": is_threat_shield_monitoring_enabled()},
}


def main():
cmd = sys.argv[1]

if cmd == 'list':
print(json.dumps({"system-info": {}, "summary": {}, "list-wans":{}, "service-status": {"service": "myservice"}, "counter": {"service": "hosts"}, "interface-traffic": {"interface": "eth0"}, "ipsec-tunnels": {}, "ovpn-tunnels": {}}))
elif cmd == 'call':
action = sys.argv[2]
if action == "system-info":
ret = system_info()
elif action == "summary":
ret = dashboard_summary()
elif action == "list-wans":
ret = list_wans()
elif action == "service-status":
args = json.loads(sys.stdin.read())
ret = service_status(args["service"])
elif action == "counter":
args = json.loads(sys.stdin.read())
ret = counter(args["service"])
elif action == "interface-traffic":
args = json.loads(sys.stdin.read())
ret = interface_traffic(args["interface"])
elif action == "ipsec-tunnels":
ret = ipsec_tunnels()
elif action == "ovpn-tunnels":
ret = ovpn_tunnels()

print(json.dumps({"result": ret}))

if __name__ == '__main__':
main()
118 changes: 118 additions & 0 deletions packages/ns-api/tests/test_ns_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import importlib.util
import sys
import types
import unittest
from importlib.machinery import SourceFileLoader
from pathlib import Path


MODULE_PATH = Path(__file__).resolve().parents[1] / 'files' / 'ns.dashboard'


def load_module():
fake_euci = types.ModuleType('euci')

class DummyEUci:
def get(self, *args, **kwargs):
return ''

def get_all(self, *args, **kwargs):
return {}

fake_euci.EUci = DummyEUci

fake_utils = types.SimpleNamespace(
get_all_wan_devices=lambda *args, **kwargs: [],
get_interface_from_device=lambda *args, **kwargs: '',
get_all_by_type=lambda *args, **kwargs: {},
)
fake_ovpn = types.SimpleNamespace(list_connected_clients=lambda *args, **kwargs: {})
fake_nethsec = types.ModuleType('nethsec')
fake_nethsec.utils = fake_utils
fake_nethsec.ovpn = fake_ovpn

original_argv = sys.argv[:]
original_euci = sys.modules.get('euci')
original_nethsec = sys.modules.get('nethsec')
sys.argv = ['ns.dashboard', 'noop']
sys.modules['euci'] = fake_euci
sys.modules['nethsec'] = fake_nethsec

try:
loader = SourceFileLoader('ns_dashboard', str(MODULE_PATH))
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
finally:
sys.argv = original_argv
if original_euci is None:
sys.modules.pop('euci', None)
else:
sys.modules['euci'] = original_euci
if original_nethsec is None:
sys.modules.pop('nethsec', None)
else:
sys.modules['nethsec'] = original_nethsec

return module


class DashboardSummaryTest(unittest.TestCase):
def test_dashboard_summary_aggregates_initial_dashboard_data(self):
module = load_module()
service_calls = []
counter_calls = []

module.system_info = lambda: {'hostname': 'fw'}
module.service_status = lambda service: service_calls.append(service) or {'status': f'{service}-status'}
module.counter = lambda service: counter_calls.append(service) or {'count': len(service)}
module.ipsec_tunnels = lambda: {'enabled': 1, 'connected': 0}
module.ovpn_tunnels = lambda: {'enabled': 2, 'connected': 1}
module.is_threat_shield_monitoring_enabled = lambda: True

summary = module.dashboard_summary()

self.assertEqual(summary['systemInfo'], {'hostname': 'fw'})
self.assertEqual(
summary['serviceStatus'],
{
'internet': 'internet-status',
'dns-configured': 'dns-configured-status',
'mwan': 'mwan-status',
'netifyd': 'netifyd-status',
'openvpn_rw': 'openvpn_rw-status',
'banip': 'banip-status',
'threat_shield_dns': 'threat_shield_dns-status',
'dedalo': 'dedalo-status',
},
)
self.assertEqual(
summary['counters'],
{
'hosts': len('hosts'),
'openvpn_rw': len('openvpn_rw'),
'threat_shield_ip': len('threat_shield_ip'),
},
)
self.assertEqual(summary['tunnels']['ipsec'], {'enabled': 1, 'connected': 0})
self.assertEqual(summary['tunnels']['ovpn'], {'enabled': 2, 'connected': 1})
self.assertEqual(summary['threatShield'], {'monitoringEnabled': True})
self.assertEqual(
service_calls,
[
'internet',
'dns-configured',
'mwan',
'netifyd',
'openvpn_rw',
'banip',
'threat_shield_dns',
'dedalo',
],
)
self.assertEqual(counter_calls, ['hosts', 'openvpn_rw', 'threat_shield_ip'])


if __name__ == '__main__':
unittest.main()
Loading