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: 11 additions & 0 deletions core/build-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ buildah add "${container}" api-server/api-server-logs /usr/local/bin/api-server-
buildah add "${container}" ui/dist /var/lib/nethserver/cluster/ui
buildah add "${container}" api-moduled/api-moduled /usr/local/bin/api-moduled
buildah add "${container}" install.sh /var/lib/nethserver/node/install.sh

# Support tunnel client (WebSocket-based support, coexists with OpenVPN-based don)
if [[ -f support-tunnel/tunnel-client-linux-amd64 ]]; then
buildah add "${container}" support-tunnel/tunnel-client-linux-amd64 /var/lib/nethserver/node/support-tunnel/tunnel-client
fi
if [[ -d support-tunnel/users.d ]]; then
buildah add "${container}" support-tunnel/users.d /var/lib/nethserver/node/support-tunnel/users.d
fi
if [[ -d support-tunnel/diagnostics.d ]]; then
buildah add "${container}" support-tunnel/diagnostics.d /var/lib/nethserver/node/support-tunnel/diagnostics.d
fi
core_env_file=$(mktemp)
cleanup_list+=("${core_env_file}")
printf "CORE_IMAGE=${repobase}/core:%s\n" "${IMAGETAG:-latest}" >> "${core_env_file}"
Expand Down
27 changes: 27 additions & 0 deletions core/imageroot/etc/systemd/system/support-tunnel.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

[Unit]
Description=Support tunnel client
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=/var/lib/nethserver/support-tunnel/support.env
ExecStart=/usr/local/bin/tunnel-client \
--url ${SUPPORT_URL} \
--key ${SYSTEM_KEY} \
--secret ${SYSTEM_SECRET} \
--node-id ${NODE_ID} \
--redis-addr ${REDIS_ADDR} \
--users-dir ${USERS_DIR} \
--diagnostics-dir ${DIAGNOSTICS_DIR} \
--exclude ${EXCLUDE_PATTERNS} \
--tls-insecure=${TLS_INSECURE} \
--users-state-file /var/lib/nethserver/support-tunnel/users-state.json
Restart=no
RuntimeMaxSec=7d
SyslogIdentifier=support-tunnel
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import json
import sys
import agent
import agent.tasks

rdb = agent.redis_connect(privileged=True)

# Discover all cluster nodes
node_ids = []
for key in rdb.scan_iter("node/*/environment"):
node_ids.append(key.split("/")[1])

if not node_ids:
json.dump({"active": False, "nodes": []}, fp=sys.stdout)
sys.exit(0)

# Fan out get-tunnel-client-status to all nodes in parallel
tasks = []
for node_id in node_ids:
tasks.append({
"agent_id": f"node/{node_id}",
"action": "get-tunnel-client-status",
"data": {},
})

results = agent.tasks.runp(
tasks,
endpoint="redis://cluster-leader",
progress_callback=agent.get_progress_callback(10, 90),
)

nodes_status = []
for result in results:
if isinstance(result, dict) and "output" in result:
nodes_status.append(result["output"])

any_active = any(n.get("active", False) for n in nodes_status)

json.dump({
"active": any_active,
"nodes": nodes_status,
}, fp=sys.stdout)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "get-tunnel-all-nodes output",
"type": "object",
"properties": {
"active": { "type": "boolean" },
"nodes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"active": { "type": "boolean" },
"session_id": { "type": "string" },
"started_at": { "type": "string" },
"node_id": { "type": "string" }
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python3

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import json
import sys
import agent

rdb = agent.redis_connect(use_replica=True)
tunnel_config = rdb.hgetall("cluster/support_tunnel")

if not tunnel_config.get("key") or not tunnel_config.get("secret"):
agent.set_status('validation-failed')
json.dump([{
"field": "support_tunnel",
"parameter": "support_tunnel",
"value": "",
"error": "tunnel_credentials_required",
}], fp=sys.stdout)
sys.exit(2)
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import json
import sys
import agent
import agent.tasks

rdb = agent.redis_connect(privileged=True)

# Discover all cluster nodes
node_ids = []
for key in rdb.scan_iter("node/*/environment"):
node_ids.append(key.split("/")[1])

if not node_ids:
print("ERROR: no cluster nodes found", file=sys.stderr)
sys.exit(1)

# Fan out start-tunnel-client to all nodes in parallel
tasks = []
for node_id in node_ids:
tasks.append({
"agent_id": f"node/{node_id}",
"action": "start-tunnel-client",
"data": {},
})

results = agent.tasks.runp(
tasks,
endpoint="redis://cluster-leader",
progress_callback=agent.get_progress_callback(10, 90),
)

# Collect per-node results
nodes = []
errors = 0
for i, result in enumerate(results):
node_id = node_ids[i]
if isinstance(result, Exception) or (isinstance(result, dict) and result.get("error")):
errors += 1
nodes.append({
"node_id": node_id,
"connected": False,
"error": str(result),
})
elif isinstance(result, dict) and "output" in result:
output = result["output"]
if isinstance(output, str):
try:
output = json.loads(output)
except json.JSONDecodeError:
output = {}
nodes.append(output)
else:
nodes.append({
"node_id": node_id,
"connected": False,
"error": "unexpected result",
})

if errors > 0:
print(f"Warning: {errors} node(s) failed to start tunnel client", file=sys.stderr)

all_connected = all(n.get("connected", False) for n in nodes)
total_services = sum(n.get("services", 0) for n in nodes)

json.dump({
"connected": all_connected,
"total_services": total_services,
"nodes_started": len(node_ids) - errors,
"nodes_failed": errors,
"total_nodes": len(node_ids),
"nodes": nodes,
}, fp=sys.stdout)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "start-tunnel-all-nodes output",
"type": "object",
"properties": {
"nodes_started": { "type": "integer" },
"nodes_failed": { "type": "integer" },
"total_nodes": { "type": "integer" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import json
import sys
import agent
import agent.tasks

rdb = agent.redis_connect(privileged=True)

# Discover all cluster nodes
node_ids = []
for key in rdb.scan_iter("node/*/environment"):
node_ids.append(key.split("/")[1])

if not node_ids:
json.dump({"nodes_stopped": 0, "nodes_failed": 0}, fp=sys.stdout)
sys.exit(0)

# Fan out stop-tunnel-client to all nodes in parallel
tasks = []
for node_id in node_ids:
tasks.append({
"agent_id": f"node/{node_id}",
"action": "stop-tunnel-client",
"data": {},
})

errors = agent.tasks.runp_brief(
tasks,
endpoint="redis://cluster-leader",
progress_callback=agent.get_progress_callback(10, 90),
)

if errors > 0:
print(f"Warning: {errors} node(s) failed to stop tunnel client", file=sys.stderr)

json.dump({
"nodes_stopped": len(node_ids) - errors,
"nodes_failed": errors,
}, fp=sys.stdout)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-3.0-or-later
#

import json
import subprocess
import sys
import os
import agent

rdb = agent.redis_connect(use_replica=True)

# Read NODE_ID from environment
node_id = os.environ.get("NODE_ID", "1")

# Check if the systemd service is active
result = subprocess.run(
["systemctl", "is-active", "support-tunnel.service"],
capture_output=True, text=True
)
active = result.stdout.strip() == "active"

# Get PID from systemd
pid = 0
if active:
pid_result = subprocess.run(
["systemctl", "show", "-p", "MainPID", "--value", "support-tunnel.service"],
capture_output=True, text=True
)
try:
pid = int(pid_result.stdout.strip())
except ValueError:
pass

# Read state from Redis
session = rdb.hgetall(f"node/{node_id}/tunnel")

json.dump({
"active": active,
"node_id": node_id,
"support_url": session.get("support_url", ""),
"system_key": session.get("system_key", ""),
"connected": session.get("connected", "") == "true",
"services": int(session.get("services_count", "0") or "0"),
"pid": pid,
}, fp=sys.stdout)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "get-tunnel-client-status output",
"type": "object",
"properties": {
"active": { "type": "boolean" },
"session_id": { "type": "string" },
"started_at": { "type": "string" },
"node_id": { "type": "string" }
}
}
Loading
Loading