diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/00validate_cluster b/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/00validate_cluster index 7f4db9ff1..da2d17695 100755 --- a/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/00validate_cluster +++ b/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/00validate_cluster @@ -9,6 +9,10 @@ import json import sys import agent +agent.set_weight("00validate_cluster", 0) +agent.set_weight("10validate_network", 0) +agent.set_weight("40update_cluster", 3) + request = json.load(sys.stdin) rdb = agent.redis_connect() diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/40update_cluster b/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/40update_cluster new file mode 100755 index 000000000..d12271764 --- /dev/null +++ b/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/40update_cluster @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import sys +import agent +import os + +update_core_response = agent.tasks.run( + agent_id='cluster', + action='update-core', + data={'nodes': [1]}, # always 1 in a new cluster + endpoint="redis://cluster-leader", +) +if update_core_response['exit_code'] != 0: + print(agent.SD_ERR + "update-core failed", file=sys.stderr) + sys.exit(1) diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/50update b/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/50update index 08368de4e..ad306c499 100755 --- a/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/50update +++ b/core/imageroot/var/lib/nethserver/cluster/actions/create-cluster/50update @@ -98,6 +98,8 @@ agent.assert_exp(add1_module_failures == 0) # Update completed successfully, enable the phonehome.timer agent.run_helper(*'systemctl enable --now phonehome.timer'.split(' ')).check_returncode() +agent.unset_env("FIRST_CONFIGURATION") + json.dump({ "ip_address": ip_address, }, fp=sys.stdout) diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/get-cluster-status/50read b/core/imageroot/var/lib/nethserver/cluster/actions/get-cluster-status/50read index 34271d81d..0dbb40511 100755 --- a/core/imageroot/var/lib/nethserver/cluster/actions/get-cluster-status/50read +++ b/core/imageroot/var/lib/nethserver/cluster/actions/get-cluster-status/50read @@ -70,8 +70,9 @@ leader_id = int(rdb.hget("cluster/environment", "NODE_ID")) leader_endpoint = rdb.hget(f"node/{leader_id}/vpn", "endpoint") or "leader.invalid:55820" local_id = int(os.environ['NODE_ID']) local_public_key = rdb.hget(f"node/{local_id}/vpn", "public_key") +initialized = os.getenv("FIRST_CONFIGURATION") != "1" -ret = { 'initialized': False, 'leader': False, 'nodes': [], 'leader_url': '', 'default_password': False, 'ui_name': cluster_ui_name, } +ret = { 'initialized': initialized, 'leader': False, 'nodes': [], 'leader_url': '', 'default_password': False, 'ui_name': cluster_ui_name, } vpn = {} default_password = hashlib.sha256(b'Nethesis,1234').hexdigest() @@ -82,7 +83,6 @@ for p in admin['passwords']: network = rdb.get('cluster/network') if network is not None: - ret["initialized"] = True # The leader must have a public endpoint: leader_hostname, _ = leader_endpoint.rsplit(":", 1) ret['leader_url'] = f'https://{leader_hostname}/cluster-admin/' diff --git a/core/imageroot/var/lib/nethserver/node/install-finalize.sh b/core/imageroot/var/lib/nethserver/node/install-finalize.sh index a635ade3d..16c8f5c03 100755 --- a/core/imageroot/var/lib/nethserver/node/install-finalize.sh +++ b/core/imageroot/var/lib/nethserver/node/install-finalize.sh @@ -110,6 +110,7 @@ echo "Grant initial permissions:" runagent python3 <<'EOF' import agent import cluster.grants +agent.set_env("FIRST_CONFIGURATION", "1") rdb = agent.redis_connect(privileged=True) cluster.grants.grant(rdb, action_clause="*", to_clause="owner", on_clause='cluster') cluster.grants.grant(rdb, action_clause="list-*", to_clause="reader", on_clause='cluster') diff --git a/core/ui/src/App.vue b/core/ui/src/App.vue index 0aa0a7a59..29af6e311 100644 --- a/core/ui/src/App.vue +++ b/core/ui/src/App.vue @@ -19,7 +19,6 @@ import ShellHeader from "@/components/shell/ShellHeader"; import SideMenu from "@/components/shell/SideMenu"; import MobileSideMenu from "@/components/shell/MobileSideMenu"; -import axios from "axios"; import WebSocketService from "@/mixins/websocket"; import { mapState, mapActions } from "vuex"; import to from "await-to-js"; @@ -187,15 +186,39 @@ export default { } }); }, + isRetryableAgentRequest(error) { + return ( + // The cluster agent may be temporarily unavailable while restarting. + error.config?.method?.toLowerCase() === "post" && + error.response?.status == 404 && + error.response?.data?.message === "client idle check failed" && + error.config?.url?.endsWith("/cluster/tasks") + ); + }, configureAxiosInterceptors() { const context = this; + const maxAgentRestartRetries = 5; // response interceptor - axios.interceptors.response.use( + this.axios.interceptors.response.use( function (response) { return response; }, - function (error) { + async function (error) { + if (context.isRetryableAgentRequest(error)) { + const retryConfig = error.config; + retryConfig.__agentRestartRetryCount = + (retryConfig.__agentRestartRetryCount || 0) + 1; + + if ( + retryConfig.__agentRestartRetryCount <= + maxAgentRestartRetries + ) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + return context.axios.request(retryConfig); + } + } + console.error(error); // print specific error message, if available