Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
789ec9e
feat(migration): cut appliance over to my collect
edospadoni Apr 23, 2026
c7487fa
feat(migration): switch alerts to native my collect after cutover
edospadoni Jun 17, 2026
98d61af
feat(migration): point my endpoints at my-proxy-prod.onrender.com
edospadoni Jun 19, 2026
4bb31f4
fix(alert): drop disable_my_alerts opt-out
edospadoni Jun 29, 2026
7546fd6
build(ns-ui): bundle nethsecurity-ui#746 backup UI for the cutover
edospadoni Jun 29, 2026
3f81c9a
feat(subscription): expose organization and enterprise plan for migra…
edospadoni Jun 29, 2026
9f00477
build(ns-ui): bump pin to include the subscription view fix (nethsecu…
edospadoni Jun 29, 2026
8149f94
fix(subscription): surface already-registered on re-register
edospadoni Jun 30, 2026
f58ea23
chore(ns-ui): bump pin to nethsecurity-ui#746 head (899b407)
edospadoni Jun 30, 2026
688672b
chore(ns-ui): bump pin to nethsecurity-ui#746 head (7402e3b)
edospadoni Jun 30, 2026
27881a8
feat(subscription): expose system_url for enterprise units
edospadoni Jun 30, 2026
8f1123e
chore(ns-ui): bump pin to nethsecurity-ui#746 head (4a0f10b)
edospadoni Jun 30, 2026
11bbba3
feat(subscription): expose community system_url too (parity with ns8)
edospadoni Jun 30, 2026
80834dd
fix(subscription): send heartbeat before inventory on register
edospadoni Jul 7, 2026
c762305
feat(ns-api): dedalo login via My Nethesis device pairing
edospadoni Jul 8, 2026
8bb5e96
chore(ns-ui): bump pin to the My Nethesis hotspot login
edospadoni Jul 8, 2026
185ec32
chore(ns-ui): bump pin to the hotspot manager link
edospadoni Jul 8, 2026
e11c355
chore(ns-ui): bump pin to the refined My Nethesis login UX
edospadoni Jul 9, 2026
efc963c
fix(ns-plug): log send failures to syslog
edospadoni Jul 9, 2026
f05dd74
fix(ns-api): return expected register/oidc errors as validation errors
edospadoni Jul 9, 2026
367748a
chore(ns-ui): bump pin to the inline login errors fix
edospadoni Jul 9, 2026
ee04169
chore(ns-ui): bump pin to the session status card
edospadoni Jul 13, 2026
91a172b
feat(ns-api): expose the my system name in subscription info
edospadoni Jul 14, 2026
e9e91db
chore(ns-ui): bump pin to the system name on subscription
edospadoni Jul 14, 2026
eaf28af
chore(ns-ui): bump pin to nethsecurity-ui#746 head (c6afb3a)
edospadoni Jul 20, 2026
27d12c0
chore(ns-ui): bump pin to nethsecurity-ui#746 head (2cacc72)
edospadoni Jul 20, 2026
2affdfd
chore(ns-ui): bump pin to nethsecurity-ui#746 head (5b489e2b)
edospadoni Jul 27, 2026
a56b828
chore(ns-ui): bump pin to nethsecurity-ui#746 head (f910296)
edospadoni Jul 28, 2026
2976303
chore(ns-ui): bump pin to nethsecurity-ui#746 head (e17c6fa)
edospadoni Aug 26, 2026
4385319
fix(migration): rotate the enterprise feed credentials too
edospadoni Aug 27, 2026
1aaa7d4
addressed some styling/fix
Tbaile Sep 8, 2026
a05e6e3
running migrate on package install
Tbaile Sep 8, 2026
a096454
removed proxy alert
Tbaile Sep 8, 2026
5ce39d7
fixes
Tbaile Sep 8, 2026
b4e045a
removed fixmes
Tbaile Sep 8, 2026
16180d2
make migration script to over to libexec, added return codes and api
Tbaile Sep 8, 2026
6d0fbd7
added migrated attribute in subscription info
Tbaile Sep 8, 2026
501c897
bumped ui
Tbaile Sep 8, 2026
5927a97
removed comment
Tbaile Sep 8, 2026
1e338a0
added extra vmalert flags
Tbaile Sep 8, 2026
93cd987
bumped versions
Tbaile Sep 8, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ recent host Ruby (4.x), so the site is built in a `ruby:3.3` container and the
- `builder/apply-patches.sh` strips the `patches/` prefix and applies each patch into the matching upstream source directory.
- `files/` is the rootfs overlay for the final image. `files/etc/uci-defaults` holds first-boot defaults.
- Runtime web stack: **nginx** serves `ns-ui` from `/www-ns` and proxies `/api/` → **ns-api-server** on `127.0.0.1:8090`; `ns-api-server` handles auth/JWT and forwards calls to ubus/rpcd handlers.
- System monitoring alerts, including HA alerts, follow the Telegraf → Victoria Metrics/vmalert → `ns-plug-alert-proxy` path rather than sending legacy portal alerts directly from service scripts.
- System monitoring alerts, including HA alerts, follow the Telegraf → Victoria Metrics/vmalert -> Mimir.
- Many local packages are thin wrappers around upstream code. When changing behavior in one of those areas, inspect the matching upstream repo first and treat the local package as integration glue.

---
Expand Down
2 changes: 1 addition & 1 deletion packages/ns-api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk

PKG_NAME:=ns-api
PKG_VERSION:=3.7.2
PKG_VERSION:=3.8.0_beta
PKG_RELEASE:=1

PKG_BUILD_DIR:=$(BUILD_DIR)/ns-api-$(PKG_VERSION)
Expand Down
5 changes: 2 additions & 3 deletions packages/ns-api/files/ns.backup
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,9 @@ elif cmd == 'call':
elif action == 'registered-delete-backup':
try:
data = json.load(sys.stdin)
p = subprocess.run(['/usr/sbin/remote-backup', 'delete', data['id']],
subprocess.run(['/usr/sbin/remote-backup', 'delete', data['id']],
check=True, capture_output=True, text=True)
# return content
print(p.stdout)
print(json.dumps({'message': 'success'}))
except subprocess.CalledProcessError as error:
print(json.dumps(utils.generic_error('remote backup delete failed')))
except KeyError as error:
Expand Down
118 changes: 108 additions & 10 deletions packages/ns-api/files/ns.dedalo
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ from euci import EUci

tmp_dir = "/var/run/"
token_file = f"{tmp_dir}/dedalo_token"
pairing_file = f"{tmp_dir}/dedalo_pairing.json"
opts = ["network", "hotspot_id", "unit_name", "unit_description", "interface"]

## Utilities
Expand All @@ -45,24 +46,104 @@ def setup(u):
def login(args):
u = EUci()
try:
p = subprocess.run(['curl', '-L', '--url', f'https://{args["host"]}/api/login', '--header', 'Content-Type: application/json', '--data-binary', json.dumps(args)], check=True, capture_output=True, text=True)
p = subprocess.run(['curl', '-L', '-m', '15', '--connect-timeout', '5', '--url', f'https://{args["host"]}/api/login', '--header', 'Content-Type: application/json', '--data-binary', json.dumps(args)], check=True, capture_output=True, text=True)
Comment thread
Tbaile marked this conversation as resolved.
resp = json.loads(p.stdout)
if 'token' in resp:
setup(u)
u.set("dedalo", "config", "splash_page", f'http://{args["host"]}/wings')
u.set("dedalo", "config", "aaa_url", f'https://{args["host"]}/wax/aaa')
u.set("dedalo", "config", "api_url", f'https://{args["host"]}/api')
u.commit("dedalo")
os.makedirs(tmp_dir, exist_ok = True)
with open(token_file, "w") as fp:
fp.write(resp["token"])
_connect_to_host(u, args["host"], resp["token"])
return {"response": "success"}
else:
return utils.generic_error("login_failed")
except Exception as e:
print(e, file=sys.stderr)
return {"success": False}


def _connect_to_host(u, host, token, account_name="", account_user=""):
# same side effects as a successful password login: point the unit at
# the chosen hotspot manager and store the session token; the account
# info (from OIDC pairing) is kept to show who the unit is linked to
setup(u)
u.set("dedalo", "config", "splash_page", f'http://{host}/wings')
u.set("dedalo", "config", "aaa_url", f'https://{host}/wax/aaa')
u.set("dedalo", "config", "api_url", f'https://{host}/api')
for opt, value in (("account_name", account_name), ("account_user", account_user)):
if value:
u.set("dedalo", "config", opt, value)
else:
try:
u.delete("dedalo", "config", opt)
except:
pass
u.commit("dedalo")
os.makedirs(tmp_dir, exist_ok = True)
with open(token_file, "w") as fp:
fp.write(token)

def oidc_start(args):
host = args.get("host") or "my.nethspot.com"
u = EUci()
unit_name = u.get("dedalo", "config", "unit_name", default="")
if not unit_name:
with open('/proc/sys/kernel/hostname', 'r') as fp:
unit_name = fp.read().strip()
try:
p = subprocess.run(['curl', '-s', '-L', '-m', '15', '--connect-timeout', '5', '-X', 'POST', '-w', '\n%{http_code}', '--url', f'https://{host}/api/auth/oidc/device/start', '--header', 'Content-Type: application/json', '--data-binary', json.dumps({"unit_name": unit_name})], check=True, capture_output=True, text=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subprocess will leak credentials, switch to python own requests package. Will likely handle most of the work you need to do to adjust the response

body, _, http_code = p.stdout.rpartition('\n')
# Expected outcomes of the user-provided host (manager without OIDC
# support, wrong/unreachable host) are validation errors: the UI
# shows them inline without the global error toast.
if http_code == '404':
# hotspot manager without OIDC device pairing support
return utils.validation_error("host", "oidc_not_supported")
resp = json.loads(body)
except Exception as e:
print(e, file=sys.stderr)
return utils.validation_error("host", "pairing_start_failed")
if 'device_code' not in resp or 'verification_url' not in resp:
return utils.validation_error("host", "pairing_start_failed")
# the device_code stays on the unit: the browser only ever sees the
# verification_url (carrying the public pair_id)
os.makedirs(tmp_dir, exist_ok = True)
fd = os.open(pairing_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, 'w') as fp:
json.dump({"host": host, "device_code": resp["device_code"]}, fp)
return {
"verification_url": resp["verification_url"],
"expires_in": resp.get("expires_in", 600),
"interval": resp.get("interval", 2),
}

def oidc_poll():
u = EUci()
try:
with open(pairing_file, 'r') as fp:
pairing = json.load(fp)
except:
return utils.generic_error("no_pairing_in_progress")
host = pairing["host"]
try:
p = subprocess.run(['curl', '-s', '-L', '-m', '15', '--connect-timeout', '5', '--url', f'https://{host}/api/auth/oidc/device/poll', '--header', 'Content-Type: application/json', '--data-binary', json.dumps({"device_code": pairing["device_code"]})], check=True, capture_output=True, text=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subprocess will leak credentials, switch to python own requests package. Will likely handle most of the work you need to do to adjust the response

resp = json.loads(p.stdout)
except Exception as e:
# transient error talking to the hotspot manager: keep polling
print(e, file=sys.stderr)
return {"status": "pending"}
status = resp.get("status", "")
if status == "ready":
os.remove(pairing_file)
_connect_to_host(u, host, resp["token"], resp.get("account_name", ""), resp.get("logged_by", ""))
return {"status": "success", "account_name": resp.get("account_name", "")}
if status == "failed":
os.remove(pairing_file)
# NB: don't name the key "error" — a top-level "error" key makes
# nethsecurity-api reply 500 (application-error convention) and the
# failed status would never reach the UI as data.
return {"status": "failed", "reason": resp.get("error", "unknown")}
if status == "expired":
os.remove(pairing_file)
return {"status": "expired"}
return {"status": "pending"}

def list_sessions():
process = subprocess.run(["/usr/bin/dedalo", "query", "list"], capture_output=True, text=True)
if not process.stdout:
Expand Down Expand Up @@ -131,7 +212,7 @@ def list_parents():
u = EUci()
try:
api_url = u.get("dedalo", "config", "api_url")
p = subprocess.run(['curl', '-L', '-s', '--url', f'{api_url}/hotspots', '--header', f"Token: {_get_token()}"], capture_output=True, text=True)
p = subprocess.run(['curl', '-L', '-s', '-m', '15', '--connect-timeout', '5', '--url', f'{api_url}/hotspots', '--header', f"Token: {_get_token()}"], capture_output=True, text=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subprocess will leak credentials, switch to python own requests package. Will likely handle most of the work you need to do to adjust the response

resp = json.loads(p.stdout)
for p in resp["data"]:
parents.append({"id": p["id"], "name": p["name"], "description": p["description"]})
Expand All @@ -148,6 +229,12 @@ def unregister():
except Exception as e:
print(e, file=sys.stderr)
return utils.generic_error("unregister_failed")
try:
u.delete("dedalo", "config", "account_name")
u.delete("dedalo", "config", "account_user")
u.commit("dedalo")
except:
pass
try:
firewall.delete_linked_sections(EUci(), "dedalo/config")
subprocess.run(["/sbin/ifdown", "dedalo"], capture_output=True, check=True)
Expand Down Expand Up @@ -178,6 +265,10 @@ def get_configuration():
with open('/proc/sys/kernel/hostname', 'r') as fp:
ret["unit_name"] = fp.read().strip()
ret["connected"] = os.path.exists(token_file)
ret["account_name"] = u.get("dedalo", "config", "account_name", default="")
ret["account_user"] = u.get("dedalo", "config", "account_user", default="")
api_url = u.get("dedalo", "config", "api_url", default="")
ret["manager_host"] = api_url.replace("https://", "").replace("/api", "")
return {"configuration": ret}

def set_configuration(args):
Expand Down Expand Up @@ -259,6 +350,8 @@ cmd = sys.argv[1]
if cmd == 'list':
print(json.dumps({
"login": {"host": "my.nethspot.com", "username": "myuser", "password": "mypassword"},
"oidc-start": {"host": "my.nethspot.com"},
"oidc-poll": {},
"list-sessions": {},
"list-parents": {},
"list-devices": {},
Expand All @@ -285,6 +378,11 @@ else:
elif action == "login":
args = json.loads(sys.stdin.read())
ret = login(args)
elif action == "oidc-start":
args = json.loads(sys.stdin.read())
ret = oidc_start(args)
elif action == "oidc-poll":
ret = oidc_poll()
elif action == "set-configuration":
args = json.loads(sys.stdin.read())
ret = set_configuration(args)
Expand Down
44 changes: 35 additions & 9 deletions packages/ns-api/files/ns.subscription
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import sys
import json
import subprocess
from datetime import datetime
from time import sleep
from nethsec import utils
from euci import EUci

Expand All @@ -20,17 +21,19 @@ def register(args):

secret = args["secret"]

try:
subprocess.run(["/usr/sbin/register", "enterprise", secret, '5'], check=True, capture_output=True)
enterprise = subprocess.run(["/usr/sbin/register", "enterprise", secret, '5'], capture_output=True)
if enterprise.returncode == 0:
return {"result": "success"}
except:
pass

try:
subprocess.run(["/usr/sbin/register", "community", secret, '5'], check=True, capture_output=True)
# Exit code 2: the system is already registered on my.
if enterprise.returncode == 2:
return utils.validation_error("secret", "system_already_registered")

community = subprocess.run(["/usr/sbin/register", "community", secret, '5'], capture_output=True)
if community.returncode == 0:
return {"result": "success"}
except:
return utils.generic_error("invalid_secret_or_server_not_found")

return utils.generic_error("invalid_secret_or_server_not_found")

def unregister():
try:
Expand Down Expand Up @@ -61,6 +64,22 @@ def info():
type = u.get('ns-plug', 'config', 'type', default='')

ret = {"server_id": data["id"], "systemd_id": data["uuid"], "plan": data["subscription"]["subscription_plan"]["name"], "expiration": expiration, "active": active, "type": type}

if type == "enterprise":
ret["organization"] = data.get("organization", "")
# The system name given on my at creation time (threaded by
# subscription-info from the collect /info payload).
ret["system_name"] = data.get("system_name", "")
ret["plan"] = "Nethesis Enterprise"
ret["system_url"] = f"https://my-proxy-prod.onrender.com/systems/{data['uuid']}"
else:
# Community: link the system to its my.nethserver.com page, matching ns8.
sub_id = (data.get("subscription") or {}).get("id")
if sub_id:
ret["system_url"] = f"https://my.nethserver.com/servers/{sub_id}"

ret["migrated"] = u.get('ns-plug', 'config', 'migrated', dtype=bool, default=False)

return ret


Expand Down Expand Up @@ -90,7 +109,8 @@ if cmd == 'list':
"unregister": {},
"info": {},
"inventory-status": {},
"send-inventory": {}
"send-inventory": {},
"migrate": {},
}))
elif cmd == 'call':
action = sys.argv[2]
Expand All @@ -105,5 +125,11 @@ elif cmd == 'call':
ret = inventory_status()
elif action == "send-inventory":
ret = send_inventory()
elif action == "migrate":
try:
subprocess.run(["/usr/libexec/migrate-to-my"], check=True, capture_output=True)
ret = {}
except subprocess.CalledProcessError:
ret = utils.generic_error("failed to migrate")

print(json.dumps(ret))
3 changes: 1 addition & 2 deletions packages/ns-ha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,7 @@ Keepalived Statistics:

HA alerts are evaluated by **vmalert** from metrics exported by `/usr/libexec/telegraf-ha-alert`.
The collector and HA alert rules are installed by the always-present `telegraf` and
`victoria-metrics` packages. When alerts fire, `ns-plug-alert-proxy` forwards the legacy HA alert IDs
to the monitoring portal if the machine has a valid registration.
`victoria-metrics` packages.

Available alerts are:

Expand Down
12 changes: 4 additions & 8 deletions packages/ns-plug/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk

PKG_NAME:=ns-plug
PKG_VERSION:=1.3.2
PKG_VERSION:=1.4.0_beta
PKG_RELEASE:=1

PKG_BUILD_DIR:=$(BUILD_DIR)/ns-plug-$(PKG_VERSION)
Expand All @@ -21,7 +21,7 @@ define Package/ns-plug
CATEGORY:=NethSecurity
TITLE:=NethSecurity controller client
URL:=https://github.com/NethServer/nethsecurity-controller/
DEPENDS:=+openvpn +lscpu +python3-nethsec +python3-yaml +telegraf +victoria-metrics
DEPENDS:=+openvpn +lscpu +python3-nethsec +python3-yaml +telegraf +victoria-metrics +jq
PKGARCH:=all
endef

Expand All @@ -44,9 +44,8 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/cron restart
/usr/libexec/ns-plug/40_ns-plug_mwan_hooks
/etc/init.d/ns-plug restart
/etc/init.d/ns-plug-alert-proxy enable
/etc/init.d/ns-plug-alert-proxy restart
/etc/init.d/vmalert reload
/usr/libexec/migrate-to-my
fi
exit 0
endef
Expand All @@ -58,8 +57,6 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
crontab -l | grep -v "/usr/sbin/send-inventory" | sort | uniq | crontab -
crontab -l | grep -v "/usr/sbin/send-heartbeat" | sort | uniq | crontab -
sed -i '/\/usr\/libexec\/ns-plug\/mwan-hooks/d' /etc/mwan3.user
/etc/init.d/ns-plug-alert-proxy stop
/etc/init.d/ns-plug-alert-proxy disable
fi
exit 0
endef
Expand All @@ -77,11 +74,10 @@ define Package/ns-plug/install
$(INSTALL_DIR) $(1)/usr/libexec/ns-plug
$(INSTALL_DIR) $(1)/usr/libexec/mwan-hooks
$(INSTALL_BIN) ./files/ns-plug.init $(1)/etc/init.d/ns-plug
$(INSTALL_BIN) ./files/ns-plug-alert-proxy.init $(1)/etc/init.d/ns-plug-alert-proxy
$(INSTALL_BIN) ./files/ns-plug $(1)/usr/sbin/ns-plug
$(INSTALL_BIN) ./files/ns-plug-alert-proxy $(1)/usr/sbin/ns-plug-alert-proxy
$(INSTALL_BIN) ./files/distfeed-setup $(1)/usr/sbin/distfeed-setup
$(INSTALL_BIN) ./files/apk-official $(1)/usr/sbin/apk-official
$(INSTALL_BIN) ./files/migrate-to-my $(1)/usr/libexec
$(INSTALL_BIN) ./files/remote-backup $(1)/usr/sbin
$(INSTALL_BIN) ./files/send-backup $(1)/usr/sbin
$(INSTALL_BIN) ./files/send-heartbeat $(1)/usr/sbin
Expand Down
2 changes: 2 additions & 0 deletions packages/ns-plug/files/config
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ config main 'config'
option unit_name ''
option tls_verify '1'
option backup_url 'https://backupd.nethesis.it'
option collect_url 'https://my-proxy-prod.onrender.com/collect/api/systems'
option notifier_url ''
option repository_url 'https://updates.nethsecurity.nethserver.org'
option channel ''
option tun_mtu ''
Expand Down
Loading
Loading