feat: parallel agent upgrades with per-host log panels (0.4.2)
Allow multiple remote update jobs at once; each upgrade gets its own bottom-right log dock that auto-closes 30s after success. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||
|
||||
APP_NAME = "Security Alert Center"
|
||||
APP_VERSION = "0.4.1"
|
||||
APP_VERSION = "0.4.2"
|
||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||
|
||||
+2
-15
@@ -4,16 +4,7 @@
|
||||
<main :class="showShell ? 'app-main' : ''">
|
||||
<RouterView />
|
||||
</main>
|
||||
<HostActionLogModal
|
||||
v-if="showShell"
|
||||
:open="hostRemoteActionLog.open"
|
||||
:title="hostRemoteActionLog.title"
|
||||
:loading="hostRemoteActionLog.loading"
|
||||
:ok="hostRemoteActionLog.ok"
|
||||
:message="hostRemoteActionLog.message"
|
||||
:output="hostRemoteActionLog.output"
|
||||
@close="closeHostRemoteActionLog"
|
||||
/>
|
||||
<HostActionLogStack v-if="showShell" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,11 +13,7 @@ import { computed, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getToken } from "./api";
|
||||
import AppSidebar from "./components/AppSidebar.vue";
|
||||
import HostActionLogModal from "./components/HostActionLogModal.vue";
|
||||
import {
|
||||
closeHostRemoteActionLog,
|
||||
hostRemoteActionLog,
|
||||
} from "./composables/useHostRemoteAction";
|
||||
import HostActionLogStack from "./components/HostActionLogStack.vue";
|
||||
import { refreshSessionRole } from "./router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
v-if="remoteJobRunning && !remoteJobPanelOpen"
|
||||
type="button"
|
||||
class="sac-nav-item sac-remote-job-badge"
|
||||
title="Обновление агента выполняется на сервере SAC"
|
||||
@click="showHostRemoteActionLog"
|
||||
title="Обновления агентов выполняются на сервере SAC"
|
||||
@click="showHostRemoteActionLog()"
|
||||
>
|
||||
<span class="sac-nav-icon sac-remote-job-spinner" aria-hidden="true" />
|
||||
<span class="sac-nav-label">Обновление…</span>
|
||||
<span class="sac-nav-label">Обновления…</span>
|
||||
</button>
|
||||
<RouterLink
|
||||
v-for="item in navItems"
|
||||
@@ -46,8 +46,8 @@ import { clearToken, isAdmin } from "../api";
|
||||
import SidebarSystemStats from "./SidebarSystemStats.vue";
|
||||
import { useSacVersion } from "../composables/useSacVersion";
|
||||
import {
|
||||
anyRemoteActionLogOpen,
|
||||
hasActiveHostRemoteAction,
|
||||
hostRemoteActionLog,
|
||||
showHostRemoteActionLog,
|
||||
} from "../composables/useHostRemoteAction";
|
||||
|
||||
@@ -55,7 +55,7 @@ const { appName, appVersion } = useSacVersion();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const remoteJobRunning = computed(() => hasActiveHostRemoteAction());
|
||||
const remoteJobPanelOpen = computed(() => hostRemoteActionLog.open);
|
||||
const remoteJobPanelOpen = computed(() => anyRemoteActionLogOpen());
|
||||
|
||||
const navItems = computed(() => {
|
||||
const items = [
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<p v-if="loading" class="host-action-log-status">
|
||||
<span class="host-action-log-spinner" aria-hidden="true" />
|
||||
Выполняется на удалённом хосте… Можно перейти в другой раздел SAC.
|
||||
Выполняется на удалённом хосте… Можно запустить обновление других хостов.
|
||||
</p>
|
||||
<p v-if="message && !loading" class="host-action-log-message" :class="messageClass">{{ message }}</p>
|
||||
<p v-else-if="message && loading" class="muted host-action-log-sub host-action-log-message">{{ message }}</p>
|
||||
@@ -56,12 +56,7 @@ const messageClass = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.host-action-log-dock {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
z-index: 1000;
|
||||
width: min(56rem, calc(100vw - 2rem));
|
||||
max-width: calc(100vw - 2rem);
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div v-if="openLogs.length" class="host-action-log-stack" aria-live="polite">
|
||||
<HostActionLogModal
|
||||
v-for="entry in openLogs"
|
||||
:key="entry.id"
|
||||
:open="true"
|
||||
:title="entry.title"
|
||||
:loading="entry.loading"
|
||||
:ok="entry.ok"
|
||||
:message="entry.message"
|
||||
:output="entry.output"
|
||||
@close="closeHostRemoteActionLog(entry.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import HostActionLogModal from "./HostActionLogModal.vue";
|
||||
import {
|
||||
closeHostRemoteActionLog,
|
||||
hostRemoteActionLogs,
|
||||
} from "../composables/useHostRemoteAction";
|
||||
|
||||
const openLogs = computed(() => hostRemoteActionLogs.filter((e) => e.open));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.host-action-log-stack {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: flex-end;
|
||||
gap: 0.65rem;
|
||||
width: min(56rem, calc(100vw - 2rem));
|
||||
max-height: min(90vh, 48rem);
|
||||
overflow-y: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.host-action-log-stack :deep(.host-action-log-panel) {
|
||||
pointer-events: auto;
|
||||
width: 100%;
|
||||
max-height: min(36rem, 45vh);
|
||||
}
|
||||
</style>
|
||||
@@ -7,11 +7,8 @@
|
||||
v-if="showUpgrade"
|
||||
type="button"
|
||||
class="agent-version-upgrade"
|
||||
:class="{
|
||||
'agent-version-upgrade--inactive': upgradeBlocked,
|
||||
'agent-version-upgrade--updating': updating,
|
||||
}"
|
||||
:disabled="updating || upgradeBlocked"
|
||||
:class="{ 'agent-version-upgrade--updating': updating }"
|
||||
:disabled="updating"
|
||||
:title="buttonTitle"
|
||||
@click.stop="emit('upgrade')"
|
||||
>
|
||||
@@ -35,8 +32,6 @@ const props = defineProps<{
|
||||
host: HostSummary;
|
||||
git: AgentGitVersions | null | undefined;
|
||||
updating?: boolean;
|
||||
/** Другой хост сейчас обновляется — стрелка видна, но неактивна (серая). */
|
||||
upgradeBlocked?: boolean;
|
||||
outdatedHighlight?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -65,9 +60,6 @@ const buttonTitle = computed(() => {
|
||||
if (props.updating) {
|
||||
return "Обновление выполняется…";
|
||||
}
|
||||
if (props.upgradeBlocked) {
|
||||
return "Дождитесь завершения обновления на другом хосте";
|
||||
}
|
||||
return upgradeTitle.value;
|
||||
});
|
||||
</script>
|
||||
@@ -127,13 +119,11 @@ const buttonTitle = computed(() => {
|
||||
background: rgba(63, 185, 80, 0.28);
|
||||
}
|
||||
|
||||
.agent-version-upgrade--inactive .agent-version-upgrade__arrow,
|
||||
.agent-version-upgrade--updating .agent-version-upgrade__arrow {
|
||||
background: rgba(139, 148, 158, 0.2);
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.agent-version-upgrade--inactive .agent-version-upgrade__target,
|
||||
.agent-version-upgrade--updating .agent-version-upgrade__target {
|
||||
color: #8b949e;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -12,60 +12,94 @@ import { emitHostPatch } from "../utils/hostPatchBus";
|
||||
|
||||
export type HostRemoteActionKind = "fallback" | "ssh-update";
|
||||
|
||||
export const hostRemoteActionLog = reactive({
|
||||
open: false,
|
||||
title: "",
|
||||
loading: false,
|
||||
ok: null as boolean | null,
|
||||
message: "",
|
||||
output: "",
|
||||
hostId: 0,
|
||||
});
|
||||
export type HostRemoteActionLogEntry = {
|
||||
id: string;
|
||||
hostId: number;
|
||||
open: boolean;
|
||||
loading: boolean;
|
||||
ok: boolean | null;
|
||||
title: string;
|
||||
message: string;
|
||||
output: string;
|
||||
};
|
||||
|
||||
export const hostRemoteActionLogs = reactive<HostRemoteActionLogEntry[]>([]);
|
||||
|
||||
const POLL_MS = 1500;
|
||||
const SUCCESS_AUTO_CLOSE_MS = 30_000;
|
||||
|
||||
let successAutoCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let nextLogSeq = 1;
|
||||
const autoCloseTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const hostRunningPromises = new Map<number, Promise<HostRemoteActionJobStatus | null>>();
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function clearSuccessAutoCloseTimer() {
|
||||
if (successAutoCloseTimer != null) {
|
||||
clearTimeout(successAutoCloseTimer);
|
||||
successAutoCloseTimer = null;
|
||||
function newLogId(): string {
|
||||
return `ra-${Date.now()}-${nextLogSeq++}`;
|
||||
}
|
||||
|
||||
function findLogById(entryId: string): HostRemoteActionLogEntry | undefined {
|
||||
return hostRemoteActionLogs.find((e) => e.id === entryId);
|
||||
}
|
||||
|
||||
function findLoadingLogForHost(hostId: number): HostRemoteActionLogEntry | undefined {
|
||||
return hostRemoteActionLogs.find((e) => e.hostId === hostId && e.loading);
|
||||
}
|
||||
|
||||
function clearAutoCloseTimer(entryId: string) {
|
||||
const timer = autoCloseTimers.get(entryId);
|
||||
if (timer != null) {
|
||||
clearTimeout(timer);
|
||||
autoCloseTimers.delete(entryId);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSuccessAutoClose() {
|
||||
clearSuccessAutoCloseTimer();
|
||||
successAutoCloseTimer = setTimeout(() => {
|
||||
hostRemoteActionLog.open = false;
|
||||
successAutoCloseTimer = null;
|
||||
}, SUCCESS_AUTO_CLOSE_MS);
|
||||
}
|
||||
|
||||
function applyJobToModal(job: HostRemoteActionJobStatus) {
|
||||
hostRemoteActionLog.title = job.title || hostRemoteActionLog.title;
|
||||
hostRemoteActionLog.message = job.message || hostRemoteActionLog.message;
|
||||
hostRemoteActionLog.output = job.output || job.stdout || "";
|
||||
}
|
||||
|
||||
function finishModal(job: HostRemoteActionJobStatus) {
|
||||
hostRemoteActionLog.loading = false;
|
||||
hostRemoteActionLog.ok = job.ok ?? job.status === "success";
|
||||
applyJobToModal(job);
|
||||
hostRemoteActionLog.open = true;
|
||||
if (hostRemoteActionLog.ok) {
|
||||
scheduleSuccessAutoClose();
|
||||
function removeClosedFinishedLog(entryId: string) {
|
||||
const idx = hostRemoteActionLogs.findIndex((e) => e.id === entryId);
|
||||
if (idx < 0) return;
|
||||
const entry = hostRemoteActionLogs[idx];
|
||||
if (!entry.loading && !entry.open) {
|
||||
hostRemoteActionLogs.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollRemoteJob(hostId: number): Promise<HostRemoteActionJobStatus> {
|
||||
function scheduleSuccessAutoClose(entryId: string) {
|
||||
clearAutoCloseTimer(entryId);
|
||||
autoCloseTimers.set(
|
||||
entryId,
|
||||
setTimeout(() => {
|
||||
const entry = findLogById(entryId);
|
||||
if (entry) {
|
||||
entry.open = false;
|
||||
removeClosedFinishedLog(entryId);
|
||||
}
|
||||
autoCloseTimers.delete(entryId);
|
||||
}, SUCCESS_AUTO_CLOSE_MS),
|
||||
);
|
||||
}
|
||||
|
||||
function applyJobToEntry(entry: HostRemoteActionLogEntry, job: HostRemoteActionJobStatus) {
|
||||
entry.title = job.title || entry.title;
|
||||
entry.message = job.message || entry.message;
|
||||
entry.output = job.output || job.stdout || "";
|
||||
}
|
||||
|
||||
function finishEntry(entry: HostRemoteActionLogEntry, job: HostRemoteActionJobStatus) {
|
||||
entry.loading = false;
|
||||
entry.ok = job.ok ?? job.status === "success";
|
||||
applyJobToEntry(entry, job);
|
||||
entry.open = true;
|
||||
if (entry.ok) {
|
||||
scheduleSuccessAutoClose(entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollRemoteJob(hostId: number, entry: HostRemoteActionLogEntry): Promise<HostRemoteActionJobStatus> {
|
||||
for (;;) {
|
||||
const job = await fetchHostRemoteJob(hostId);
|
||||
applyJobToModal(job);
|
||||
applyJobToEntry(entry, job);
|
||||
if (!job.active && job.status !== "running") {
|
||||
return job;
|
||||
}
|
||||
@@ -99,20 +133,12 @@ async function startRemoteAction(hostId: number, kind: HostRemoteActionKind) {
|
||||
await updateHostAgentViaSsh(hostId);
|
||||
}
|
||||
|
||||
export async function runHostRemoteAction(
|
||||
async function executeRemoteAction(
|
||||
hostId: number,
|
||||
title: string,
|
||||
kind: HostRemoteActionKind,
|
||||
entry: HostRemoteActionLogEntry,
|
||||
): Promise<HostRemoteActionJobStatus | null> {
|
||||
clearSuccessAutoCloseTimer();
|
||||
hostRemoteActionLog.open = true;
|
||||
hostRemoteActionLog.loading = true;
|
||||
hostRemoteActionLog.ok = null;
|
||||
hostRemoteActionLog.title = title;
|
||||
hostRemoteActionLog.message = "Запуск на сервере SAC…";
|
||||
hostRemoteActionLog.output = "";
|
||||
hostRemoteActionLog.hostId = hostId;
|
||||
|
||||
try {
|
||||
try {
|
||||
await startRemoteAction(hostId, kind);
|
||||
@@ -123,31 +149,93 @@ export async function runHostRemoteAction(
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
hostRemoteActionLog.message =
|
||||
"Выполняется на удалённом хосте… Можно перейти в другой раздел SAC.";
|
||||
const job = await pollRemoteJob(hostId);
|
||||
finishModal(job);
|
||||
entry.message = "Выполняется на удалённом хосте… Можно запустить обновление других хостов.";
|
||||
const job = await pollRemoteJob(hostId, entry);
|
||||
finishEntry(entry, job);
|
||||
await patchHostFromJob(hostId, job);
|
||||
return job;
|
||||
} catch (e) {
|
||||
hostRemoteActionLog.loading = false;
|
||||
hostRemoteActionLog.ok = false;
|
||||
hostRemoteActionLog.message = e instanceof Error ? e.message : "Ошибка выполнения";
|
||||
entry.loading = false;
|
||||
entry.ok = false;
|
||||
entry.message = e instanceof Error ? e.message : "Ошибка выполнения";
|
||||
entry.open = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function closeHostRemoteActionLog() {
|
||||
clearSuccessAutoCloseTimer();
|
||||
hostRemoteActionLog.open = false;
|
||||
}
|
||||
export async function runHostRemoteAction(
|
||||
hostId: number,
|
||||
title: string,
|
||||
kind: HostRemoteActionKind,
|
||||
): Promise<HostRemoteActionJobStatus | null> {
|
||||
const existingPromise = hostRunningPromises.get(hostId);
|
||||
if (existingPromise) {
|
||||
const existing = findLoadingLogForHost(hostId);
|
||||
if (existing) {
|
||||
existing.open = true;
|
||||
}
|
||||
return existingPromise;
|
||||
}
|
||||
|
||||
export function showHostRemoteActionLog() {
|
||||
if (hostRemoteActionLog.loading || hostRemoteActionLog.ok != null) {
|
||||
hostRemoteActionLog.open = true;
|
||||
const entry: HostRemoteActionLogEntry = {
|
||||
id: newLogId(),
|
||||
hostId,
|
||||
open: true,
|
||||
loading: true,
|
||||
ok: null,
|
||||
title,
|
||||
message: "Запуск на сервере SAC…",
|
||||
output: "",
|
||||
};
|
||||
hostRemoteActionLogs.push(entry);
|
||||
|
||||
const promise = executeRemoteAction(hostId, title, kind, entry);
|
||||
hostRunningPromises.set(hostId, promise);
|
||||
try {
|
||||
return await promise;
|
||||
} finally {
|
||||
hostRunningPromises.delete(hostId);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasActiveHostRemoteAction(): boolean {
|
||||
return hostRemoteActionLog.loading;
|
||||
export function closeHostRemoteActionLog(entryId: string) {
|
||||
clearAutoCloseTimer(entryId);
|
||||
const entry = findLogById(entryId);
|
||||
if (!entry) return;
|
||||
entry.open = false;
|
||||
if (!entry.loading) {
|
||||
removeClosedFinishedLog(entryId);
|
||||
}
|
||||
}
|
||||
|
||||
export function showHostRemoteActionLog(hostId?: number) {
|
||||
if (hostId != null) {
|
||||
for (const entry of hostRemoteActionLogs) {
|
||||
if (entry.hostId === hostId && (entry.loading || entry.ok != null)) {
|
||||
entry.open = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const entry of hostRemoteActionLogs) {
|
||||
if (entry.loading || entry.ok === false) {
|
||||
entry.open = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isHostRemoteActionActive(hostId: number): boolean {
|
||||
return hostRemoteActionLogs.some((e) => e.hostId === hostId && e.loading);
|
||||
}
|
||||
|
||||
export function hasActiveHostRemoteAction(): boolean {
|
||||
return hostRemoteActionLogs.some((e) => e.loading);
|
||||
}
|
||||
|
||||
export function hasOpenRemoteActionLogForHost(hostId: number): boolean {
|
||||
return hostRemoteActionLogs.some((e) => e.hostId === hostId && e.open);
|
||||
}
|
||||
|
||||
export function anyRemoteActionLogOpen(): boolean {
|
||||
return hostRemoteActionLogs.some((e) => e.open);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
export const APP_NAME = "Security Alert Center";
|
||||
export const APP_VERSION = "0.4.0";
|
||||
export const APP_VERSION = "0.4.2";
|
||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
v-if="host"
|
||||
:host="host"
|
||||
:git="agentGitVersions"
|
||||
:updating="updatingAgent || runningAgentFallback"
|
||||
:updating="isHostRemoteActionActive(hostId)"
|
||||
:outdated-highlight="host.version_status === 'outdated'"
|
||||
@upgrade="runAgentUpgradeFromCell"
|
||||
/>
|
||||
@@ -91,13 +91,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="runningAgentFallback"
|
||||
:disabled="isHostRemoteActionActive(hostId)"
|
||||
@click="runAgentFallback"
|
||||
>
|
||||
{{ runningAgentFallback ? "Обновление…" : "Обновить через WinRM" }}
|
||||
{{ isHostRemoteActionActive(hostId) ? "Обновление…" : "Обновить через WinRM" }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="agentControlMessage && !hostRemoteActionLog.open" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
||||
<p v-if="agentControlMessage && !hasOpenRemoteActionLogForHost(hostId)" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
||||
</div>
|
||||
<div v-if="isLinuxHost" class="host-actions">
|
||||
<div class="host-actions-row">
|
||||
@@ -119,10 +119,10 @@
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="updatingAgent"
|
||||
:disabled="isHostRemoteActionActive(hostId)"
|
||||
@click="runAgentUpdate"
|
||||
>
|
||||
{{ updatingAgent ? "Обновление…" : "Обновить ssh-monitor (SSH)" }}
|
||||
{{ isHostRemoteActionActive(hostId) ? "Обновление…" : "Обновить ssh-monitor (SSH)" }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -135,13 +135,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="runningAgentFallback"
|
||||
:disabled="isHostRemoteActionActive(hostId)"
|
||||
@click="runAgentFallback"
|
||||
>
|
||||
{{ runningAgentFallback ? "Fallback…" : "Fallback SSH" }}
|
||||
{{ isHostRemoteActionActive(hostId) ? "Fallback…" : "Fallback SSH" }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="agentControlMessage && !hostRemoteActionLog.open" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
||||
<p v-if="agentControlMessage && !hasOpenRemoteActionLogForHost(hostId)" :class="agentControlOk ? 'success' : 'error'">{{ agentControlMessage }}</p>
|
||||
<div v-if="showSshTestFeedback && (sshTestMessage || sshTestOutput)" class="host-action-feedback">
|
||||
<p :class="sshTestOk ? 'success' : 'error'">{{ sshTestMessage }}</p>
|
||||
<pre v-if="sshTestOutput" class="host-ssh-output">{{ sshTestOutput }}</pre>
|
||||
@@ -289,7 +289,8 @@ import { computed, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
|
||||
import {
|
||||
hostRemoteActionLog,
|
||||
hasOpenRemoteActionLogForHost,
|
||||
isHostRemoteActionActive,
|
||||
runHostRemoteAction,
|
||||
} from "../composables/useHostRemoteAction";
|
||||
import {
|
||||
@@ -338,13 +339,11 @@ const winRmTestMessage = ref("");
|
||||
const winRmTestOk = ref(false);
|
||||
const showWinRmTestFeedback = ref(false);
|
||||
const testingSsh = ref(false);
|
||||
const updatingAgent = ref(false);
|
||||
const showSshTestFeedback = ref(false);
|
||||
const sshTestMessage = ref("");
|
||||
const sshTestOk = ref(false);
|
||||
const sshTestOutput = ref("");
|
||||
const requestingAgentUpdate = ref(false);
|
||||
const runningAgentFallback = ref(false);
|
||||
const agentControlMessage = ref("");
|
||||
const agentControlOk = ref(false);
|
||||
const savingAgentConfig = ref(false);
|
||||
@@ -637,22 +636,17 @@ async function runRequestAgentUpdate() {
|
||||
}
|
||||
|
||||
async function runAgentFallback() {
|
||||
runningAgentFallback.value = true;
|
||||
agentControlMessage.value = "";
|
||||
const title = isWindowsHost.value ? "Обновление через WinRM" : "Fallback SSH (ssh-monitor)";
|
||||
try {
|
||||
const job = await runHostRemoteAction(hostId.value, title, "fallback");
|
||||
if (job) {
|
||||
agentControlOk.value = job.ok ?? false;
|
||||
if (job.ok) {
|
||||
agentControlMessage.value = "";
|
||||
await loadHost();
|
||||
} else {
|
||||
agentControlMessage.value = job.message || "";
|
||||
}
|
||||
const job = await runHostRemoteAction(hostId.value, title, "fallback");
|
||||
if (job) {
|
||||
agentControlOk.value = job.ok ?? false;
|
||||
if (job.ok) {
|
||||
agentControlMessage.value = "";
|
||||
await loadHost();
|
||||
} else {
|
||||
agentControlMessage.value = job.message || "";
|
||||
}
|
||||
} finally {
|
||||
runningAgentFallback.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,23 +702,18 @@ async function loadHost() {
|
||||
}
|
||||
|
||||
async function runAgentUpdate() {
|
||||
updatingAgent.value = true;
|
||||
try {
|
||||
const job = await runHostRemoteAction(
|
||||
hostId.value,
|
||||
"Обновление ssh-monitor (SSH)",
|
||||
"ssh-update",
|
||||
);
|
||||
if (job?.ok) {
|
||||
const detail = await fetchHost(hostId.value);
|
||||
if (job.product_version) {
|
||||
detail.product_version = job.product_version;
|
||||
}
|
||||
applyHostDetail(detail);
|
||||
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
||||
const job = await runHostRemoteAction(
|
||||
hostId.value,
|
||||
"Обновление ssh-monitor (SSH)",
|
||||
"ssh-update",
|
||||
);
|
||||
if (job?.ok) {
|
||||
const detail = await fetchHost(hostId.value);
|
||||
if (job.product_version) {
|
||||
detail.product_version = job.product_version;
|
||||
}
|
||||
} finally {
|
||||
updatingAgent.value = false;
|
||||
applyHostDetail(detail);
|
||||
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,27 +721,21 @@ async function runAgentUpgradeFromCell() {
|
||||
if (!host.value) return;
|
||||
const kind = remoteActionKindForHost(host.value);
|
||||
if (!kind) return;
|
||||
if (kind === "ssh-update") {
|
||||
await runAgentUpdate();
|
||||
if (isHostRemoteActionActive(hostId.value)) {
|
||||
return;
|
||||
}
|
||||
runningAgentFallback.value = true;
|
||||
try {
|
||||
const job = await runHostRemoteAction(
|
||||
hostId.value,
|
||||
remoteActionTitleForHost(host.value),
|
||||
kind,
|
||||
);
|
||||
if (job?.ok) {
|
||||
const detail = await fetchHost(hostId.value);
|
||||
if (job.product_version) {
|
||||
detail.product_version = job.product_version;
|
||||
}
|
||||
applyHostDetail(detail);
|
||||
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
||||
const job = await runHostRemoteAction(
|
||||
hostId.value,
|
||||
remoteActionTitleForHost(host.value),
|
||||
kind,
|
||||
);
|
||||
if (job?.ok) {
|
||||
const detail = await fetchHost(hostId.value);
|
||||
if (job.product_version) {
|
||||
detail.product_version = job.product_version;
|
||||
}
|
||||
} finally {
|
||||
runningAgentFallback.value = false;
|
||||
applyHostDetail(detail);
|
||||
recalcVersionStatusFromGit(detail, agentGitVersions.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,7 @@
|
||||
<HostAgentVersionUpgrade
|
||||
:host="h"
|
||||
:git="data"
|
||||
:updating="upgradingHostId === h.id"
|
||||
:upgrade-blocked="upgradingHostId != null && upgradingHostId !== h.id"
|
||||
:updating="isHostRemoteActionActive(h.id)"
|
||||
:outdated-highlight="isVersionOutdated(h)"
|
||||
@upgrade="startAgentUpgrade(h)"
|
||||
/>
|
||||
@@ -132,7 +131,11 @@ import {
|
||||
remoteActionKindForHost,
|
||||
remoteActionTitleForHost,
|
||||
} from "../utils/hostAgentUpgrade";
|
||||
import { runHostRemoteAction } from "../composables/useHostRemoteAction";
|
||||
import {
|
||||
isHostRemoteActionActive,
|
||||
runHostRemoteAction,
|
||||
showHostRemoteActionLog,
|
||||
} from "../composables/useHostRemoteAction";
|
||||
import HostAgentVersionUpgrade from "../components/HostAgentVersionUpgrade.vue";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -197,7 +200,6 @@ const initialSort = loadHostsSortPrefs();
|
||||
const sortBy = ref<SortKey>(initialSort.sortBy);
|
||||
const sortDir = ref<"asc" | "desc">(initialSort.sortDir);
|
||||
const deletingId = ref<number | null>(null);
|
||||
const upgradingHostId = ref<number | null>(null);
|
||||
let patchingHostRow = false;
|
||||
let unsubscribeHostPatch: (() => void) | null = null;
|
||||
|
||||
@@ -234,17 +236,16 @@ function isVersionOutdated(h: HostSummary): boolean {
|
||||
}
|
||||
|
||||
async function startAgentUpgrade(h: HostSummary) {
|
||||
if (!isGitAgentUpgradeAvailable(h, data.value) || upgradingHostId.value != null) {
|
||||
if (!isGitAgentUpgradeAvailable(h, data.value)) {
|
||||
return;
|
||||
}
|
||||
if (isHostRemoteActionActive(h.id)) {
|
||||
showHostRemoteActionLog(h.id);
|
||||
return;
|
||||
}
|
||||
const kind = remoteActionKindForHost(h);
|
||||
if (!kind) return;
|
||||
upgradingHostId.value = h.id;
|
||||
try {
|
||||
await runHostRemoteAction(h.id, remoteActionTitleForHost(h), kind);
|
||||
} finally {
|
||||
upgradingHostId.value = null;
|
||||
}
|
||||
void runHostRemoteAction(h.id, remoteActionTitleForHost(h), kind);
|
||||
}
|
||||
|
||||
function openHost(id: number) {
|
||||
|
||||
Reference in New Issue
Block a user