mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 22:01:59 -06:00
Implemented Remote Agent Updater Script
This commit is contained in:
@@ -18,9 +18,7 @@ import {
|
||||
MenuItem,
|
||||
Popover,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Snackbar,
|
||||
Alert
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
@@ -63,8 +61,6 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
// Track selection by agent id to avoid duplicate hostname collisions
|
||||
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
||||
const [quickJobOpen, setQuickJobOpen] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [updateStatus, setUpdateStatus] = useState({ open: false, severity: "success", message: "" });
|
||||
|
||||
// Saved custom views (from server)
|
||||
const [views, setViews] = useState([]); // [{id, name, columns:[id], filters:{}}]
|
||||
@@ -426,49 +422,6 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
});
|
||||
};
|
||||
|
||||
const triggerSilentUpdate = useCallback(async () => {
|
||||
const hostnames = Array.from(
|
||||
new Set(
|
||||
rows
|
||||
.filter((r) => selectedIds.has(r.id))
|
||||
.map((r) => r.hostname)
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
if (!hostnames.length) return;
|
||||
|
||||
setUpdateLoading(true);
|
||||
try {
|
||||
const resp = await fetch('/api/agents/silent_update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hostnames }),
|
||||
});
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await resp.json();
|
||||
} catch (err) {
|
||||
payload = null;
|
||||
}
|
||||
if (!resp.ok || (payload && payload.error)) {
|
||||
const msg = payload && payload.error ? payload.error : `HTTP ${resp.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
const count = Array.isArray(payload?.results) ? payload.results.length : hostnames.length;
|
||||
const requestId = typeof payload?.request_id === 'string' && payload.request_id ? payload.request_id : '';
|
||||
setUpdateStatus({
|
||||
open: true,
|
||||
severity: 'success',
|
||||
message: `Silent update triggered for ${count} device${count === 1 ? '' : 's'}${requestId ? ` (request ${requestId})` : ''}.`,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setUpdateStatus({ open: true, severity: 'error', message: `Failed to trigger silent update: ${message}` });
|
||||
} finally {
|
||||
setUpdateLoading(false);
|
||||
}
|
||||
}, [rows, selectedIds]);
|
||||
|
||||
// Column drag handlers
|
||||
const onHeaderDragStart = (colId) => (e) => {
|
||||
dragColId.current = colId;
|
||||
@@ -620,22 +573,6 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
</Box>
|
||||
{/* Second row: Quick Job button aligned under header title */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={selectedIds.size === 0 || updateLoading}
|
||||
onClick={triggerSilentUpdate}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
bgcolor: selectedIds.size === 0 || updateLoading ? "#2d4a6a" : "#1f6feb",
|
||||
color: "#fff",
|
||||
'&:hover': {
|
||||
bgcolor: selectedIds.size === 0 || updateLoading ? "#2d4a6a" : "#388bfd"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{updateLoading ? "Updating..." : "Update Agent"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@@ -1017,23 +954,6 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
hostnames={rows.filter((r) => selectedIds.has(r.id)).map((r) => r.hostname)}
|
||||
/>
|
||||
)}
|
||||
<Snackbar
|
||||
open={updateStatus.open}
|
||||
autoHideDuration={6000}
|
||||
onClose={(_event, reason) => {
|
||||
if (reason === 'clickaway') return;
|
||||
setUpdateStatus((prev) => ({ ...prev, open: false }));
|
||||
}}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
onClose={() => setUpdateStatus((prev) => ({ ...prev, open: false }))}
|
||||
severity={updateStatus.severity}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{updateStatus.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
{assignDialogOpen && (
|
||||
<Popover
|
||||
open={assignDialogOpen}
|
||||
|
||||
@@ -3126,48 +3126,6 @@ def scripts_quick_run():
|
||||
return jsonify({"results": results})
|
||||
|
||||
|
||||
@app.route("/api/agents/silent_update", methods=["POST"])
|
||||
def agent_silent_update():
|
||||
"""Request connected agents to run the Borealis silent update workflow."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
raw_hosts = data.get("hostnames")
|
||||
if not isinstance(raw_hosts, list):
|
||||
return jsonify({"error": "hostnames[] must be provided"}), 400
|
||||
|
||||
hostnames: List[str] = []
|
||||
seen: Set[str] = set()
|
||||
for entry in raw_hosts:
|
||||
name = str(entry or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
key = name.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
hostnames.append(name)
|
||||
|
||||
if not hostnames:
|
||||
return jsonify({"error": "No valid hostnames provided"}), 400
|
||||
|
||||
request_id = uuid.uuid4().hex
|
||||
results: List[Dict[str, str]] = []
|
||||
for host in hostnames:
|
||||
payload = {
|
||||
"target_hostname": host,
|
||||
"request_id": request_id,
|
||||
"requested_at": int(time.time()),
|
||||
}
|
||||
socketio.emit("agent_silent_update", payload)
|
||||
results.append({"hostname": host, "status": "queued"})
|
||||
|
||||
_write_service_log(
|
||||
"server",
|
||||
f"[silent_update] request_id={request_id} dispatched to {len(hostnames)} host(s): {', '.join(hostnames)}",
|
||||
)
|
||||
|
||||
return jsonify({"results": results, "request_id": request_id})
|
||||
|
||||
|
||||
@app.route("/api/ansible/quick_run", methods=["POST"])
|
||||
def ansible_quick_run():
|
||||
"""Queue an Ansible Playbook Quick Job via WebSocket to targeted agents.
|
||||
@@ -3337,27 +3295,6 @@ def handle_quick_job_result(data):
|
||||
print(f"[ERROR] quick_job_result DB update failed for job {job_id}: {e}")
|
||||
|
||||
|
||||
@socketio.on("agent_silent_update_status")
|
||||
def handle_agent_silent_update_status(data):
|
||||
try:
|
||||
hostname = str(data.get("hostname") or "").strip()
|
||||
status = str(data.get("status") or "").strip() or "unknown"
|
||||
request_id = str(data.get("request_id") or "").strip()
|
||||
error = str(data.get("error") or "").strip()
|
||||
message_parts = [
|
||||
"[silent_update_status]",
|
||||
f"host={hostname or 'unknown'}",
|
||||
f"status={status}",
|
||||
]
|
||||
if request_id:
|
||||
message_parts.append(f"request_id={request_id}")
|
||||
if error:
|
||||
message_parts.append(f"error={error}")
|
||||
_write_service_log("server", " ".join(message_parts))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Ansible Runtime API (Play Recaps)
|
||||
# ---------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user