Added server initiated Agent update

This commit is contained in:
Muhammad Ibrahim
2025-10-28 21:49:19 +00:00
parent 77a945a5b6
commit 79317b0052
10 changed files with 133 additions and 1 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1430,6 +1430,69 @@ router.patch(
},
);
// Force agent update for specific host
router.post(
"/:hostId/force-agent-update",
authenticateToken,
requireManageHosts,
async (req, res) => {
try {
const { hostId } = req.params;
// Get host to verify it exists
const host = await prisma.hosts.findUnique({
where: { id: hostId },
});
if (!host) {
return res.status(404).json({ error: "Host not found" });
}
// Get queue manager
const { QUEUE_NAMES } = require("../services/automation");
const queueManager = req.app.locals.queueManager;
if (!queueManager) {
return res.status(500).json({
error: "Queue manager not available",
});
}
// Get the agent-commands queue
const queue = queueManager.queues[QUEUE_NAMES.AGENT_COMMANDS];
// Add job to queue
await queue.add(
"update_agent",
{
api_id: host.api_id,
type: "update_agent",
},
{
attempts: 3,
backoff: {
type: "exponential",
delay: 2000,
},
},
);
res.json({
success: true,
message: "Agent update queued successfully",
host: {
id: host.id,
friendlyName: host.friendly_name,
apiId: host.api_id,
},
});
} catch (error) {
console.error("Force agent update error:", error);
res.status(500).json({ error: "Failed to force agent update" });
}
},
);
// Serve the installation script (requires API authentication)
router.get("/install", async (req, res) => {
try {

View File

@@ -176,6 +176,15 @@ function pushSettingsUpdate(apiId, newInterval) {
);
}
function pushUpdateAgent(apiId) {
const ws = apiIdToSocket.get(apiId);
safeSend(ws, JSON.stringify({ type: "update_agent" }));
}
function getConnectionByApiId(apiId) {
return apiIdToSocket.get(apiId);
}
function pushUpdateNotification(apiId, updateInfo) {
const ws = apiIdToSocket.get(apiId);
if (ws && ws.readyState === WebSocket.OPEN) {
@@ -330,10 +339,12 @@ module.exports = {
broadcastSettingsUpdate,
pushReportNow,
pushSettingsUpdate,
pushUpdateAgent,
pushUpdateNotification,
pushUpdateNotificationToAll,
// Expose read-only view of connected agents
getConnectedApiIds: () => Array.from(apiIdToSocket.keys()),
getConnectionByApiId,
isConnected: (apiId) => {
const ws = apiIdToSocket.get(apiId);
return !!ws && ws.readyState === WebSocket.OPEN;

View File

@@ -190,6 +190,19 @@ class QueueManager {
// For settings update, we need additional data
const { update_interval } = job.data;
agentWs.pushSettingsUpdate(api_id, update_interval);
} else if (type === "update_agent") {
// Force agent to update by sending WebSocket command
const ws = agentWs.getConnectionByApiId(api_id);
if (ws && ws.readyState === 1) {
// WebSocket.OPEN
agentWs.pushUpdateAgent(api_id);
console.log(`✅ Update command sent to agent ${api_id}`);
} else {
console.error(`❌ Agent ${api_id} is not connected`);
throw new Error(
`Agent ${api_id} is not connected. Cannot send update command.`,
);
}
} else {
console.error(`Unknown agent command type: ${type}`);
}

View File

@@ -187,6 +187,16 @@ const HostDetail = () => {
},
});
// Force agent update mutation
const forceAgentUpdateMutation = useMutation({
mutationFn: () =>
adminHostsAPI.forceAgentUpdate(hostId).then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries(["host", hostId]);
queryClient.invalidateQueries(["hosts"]);
},
});
const updateFriendlyNameMutation = useMutation({
mutationFn: (friendlyName) =>
adminHostsAPI
@@ -703,6 +713,29 @@ const HostDetail = () => {
/>
</button>
</div>
<div>
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5">
Force Update
</p>
<button
type="button"
onClick={() => forceAgentUpdateMutation.mutate()}
disabled={forceAgentUpdateMutation.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-md hover:bg-primary-100 dark:hover:bg-primary-900/40 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw
className={`h-3 w-3 ${
forceAgentUpdateMutation.isPending
? "animate-spin"
: ""
}`}
/>
{forceAgentUpdateMutation.isPending
? "Updating..."
: "Update Now"}
</button>
</div>
</div>
</div>
)}

View File

@@ -95,6 +95,7 @@ export const adminHostsAPI = {
api.put("/hosts/bulk/groups", { hostIds, groupIds }),
toggleAutoUpdate: (hostId, autoUpdate) =>
api.patch(`/hosts/${hostId}/auto-update`, { auto_update: autoUpdate }),
forceAgentUpdate: (hostId) => api.post(`/hosts/${hostId}/force-agent-update`),
updateFriendlyName: (hostId, friendlyName) =>
api.patch(`/hosts/${hostId}/friendly-name`, {
friendly_name: friendlyName,

View File

@@ -1797,7 +1797,12 @@ create_agent_version() {
cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/"
print_status "Agent version management removed - using file-based approach"
# Ensure we close the conditional and the function properly
fi
# Make agent binaries executable
if [ -d "$APP_DIR/agents" ]; then
chmod +x "$APP_DIR/agents/patchmon-agent-linux-"* 2>/dev/null || true
print_status "Agent binaries made executable"
fi
return 0
@@ -3095,6 +3100,12 @@ update_installation() {
print_info "Building frontend..."
npm run build
# Make agent binaries executable
if [ -d "$instance_dir/agents" ]; then
chmod +x "$instance_dir/agents/patchmon-agent-linux-"* 2>/dev/null || true
print_status "Agent binaries made executable"
fi
# Run database migrations with self-healing
print_info "Running database migrations..."
cd "$instance_dir/backend"