mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
Add Proxmox guest auto sync (#1053)
This commit is contained in:
@@ -276,6 +276,9 @@ export function registerHostBulkRoutes(
|
|||||||
dockerPatterns: existing.dockerPatterns ?? "docker",
|
dockerPatterns: existing.dockerPatterns ?? "docker",
|
||||||
preferredPrefixes:
|
preferredPrefixes:
|
||||||
existing.preferredPrefixes ?? "10., 192.168.",
|
existing.preferredPrefixes ?? "10., 192.168.",
|
||||||
|
autoSyncEnabled: existing.autoSyncEnabled ?? false,
|
||||||
|
syncIntervalMinutes: existing.syncIntervalMinutes ?? 15,
|
||||||
|
markMissingGuests: existing.markMissingGuests ?? true,
|
||||||
};
|
};
|
||||||
await db
|
await db
|
||||||
.update(hosts)
|
.update(hosts)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -70,9 +70,23 @@ export type GuacamoleAuthType = "password" | "credential";
|
|||||||
|
|
||||||
export interface ProxmoxConfig {
|
export interface ProxmoxConfig {
|
||||||
defaultCredentialId: number | null;
|
defaultCredentialId: number | null;
|
||||||
|
defaultAuthType?: string;
|
||||||
windowsPatterns: string;
|
windowsPatterns: string;
|
||||||
dockerPatterns: string;
|
dockerPatterns: string;
|
||||||
preferredPrefixes: string;
|
preferredPrefixes: string;
|
||||||
|
autoSyncEnabled?: boolean;
|
||||||
|
syncIntervalMinutes?: number;
|
||||||
|
markMissingGuests?: boolean;
|
||||||
|
lastSyncAt?: string;
|
||||||
|
lastSyncStatus?: "success" | "error";
|
||||||
|
lastSyncError?: string | null;
|
||||||
|
lastSyncResult?: {
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
markedMissing: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HostFeatureFlags {
|
export interface HostFeatureFlags {
|
||||||
|
|||||||
@@ -14,3 +14,11 @@ export interface ProxmoxDiscoverResult {
|
|||||||
credentialId: number | null;
|
credentialId: number | null;
|
||||||
defaultCredentialId: number | null;
|
defaultCredentialId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProxmoxSyncResult {
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
markedMissing: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ export type Host = {
|
|||||||
windowsPatterns: string;
|
windowsPatterns: string;
|
||||||
dockerPatterns: string;
|
dockerPatterns: string;
|
||||||
preferredPrefixes: string;
|
preferredPrefixes: string;
|
||||||
|
autoSyncEnabled?: boolean;
|
||||||
|
syncIntervalMinutes?: number;
|
||||||
|
markMissingGuests?: boolean;
|
||||||
|
lastSyncAt?: string;
|
||||||
|
lastSyncStatus?: "success" | "error";
|
||||||
|
lastSyncError?: string | null;
|
||||||
|
lastSyncResult?: {
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
markedMissing: number;
|
||||||
|
skipped: number;
|
||||||
|
errors: string[];
|
||||||
|
};
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
statsConfig?: {
|
statsConfig?: {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "@/main-axios";
|
} from "@/main-axios";
|
||||||
import type { SSHHost, SSHHostData, ProxyNode } from "@/types/index";
|
import type { SSHHost, SSHHostData, ProxyNode } from "@/types/index";
|
||||||
import type { ServerStatus, SSHHostWithStatus } from "@/main-axios";
|
import type { ServerStatus, SSHHostWithStatus } from "@/main-axios";
|
||||||
import type { ProxmoxDiscoverResult } from "@/types/proxmox";
|
import type { ProxmoxDiscoverResult, ProxmoxSyncResult } from "@/types/proxmox";
|
||||||
import {
|
import {
|
||||||
getCachedSSHHosts,
|
getCachedSSHHosts,
|
||||||
invalidateHostsAndStatusCaches,
|
invalidateHostsAndStatusCaches,
|
||||||
@@ -174,6 +174,21 @@ export async function discoverProxmoxGuests(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncProxmoxGuests(
|
||||||
|
hostId: number,
|
||||||
|
): Promise<ProxmoxSyncResult> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post(
|
||||||
|
"/proxmox/sync",
|
||||||
|
{ hostId },
|
||||||
|
{ timeout: 120000 },
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleApiError(error, "sync Proxmox guests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function bulkUpdateSSHHosts(
|
export async function bulkUpdateSSHHosts(
|
||||||
hostIds: number[],
|
hostIds: number[],
|
||||||
updates: Record<string, unknown>,
|
updates: Record<string, unknown>,
|
||||||
|
|||||||
@@ -138,6 +138,18 @@ export function ProxmoxDiscoverDialog({
|
|||||||
enableDocker: g.enableDocker,
|
enableDocker: g.enableDocker,
|
||||||
connectionType: g.connectionType,
|
connectionType: g.connectionType,
|
||||||
tags: ["proxmox", g.type, g.node],
|
tags: ["proxmox", g.type, g.node],
|
||||||
|
proxmoxConfig: {
|
||||||
|
source: {
|
||||||
|
source: "proxmox",
|
||||||
|
sourceHostId: Number(effectiveHostId),
|
||||||
|
node: g.node,
|
||||||
|
vmid: g.vmid,
|
||||||
|
type: g.type,
|
||||||
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
lastStatus: g.status,
|
||||||
|
missingSince: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const result = await bulkImportSSHHosts(toImport, false);
|
const result = await bulkImportSSHHosts(toImport, false);
|
||||||
|
|||||||
@@ -607,6 +607,21 @@
|
|||||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||||
|
"proxmoxAutoSync": "Auto sync guests",
|
||||||
|
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||||
|
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||||
|
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||||
|
"proxmoxMarkMissing": "Mark missing guests",
|
||||||
|
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||||
|
"proxmoxLastSync": "Last sync",
|
||||||
|
"proxmoxLastSyncNever": "Not synced yet",
|
||||||
|
"proxmoxLastSyncNoResult": "No result details",
|
||||||
|
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||||
|
"proxmoxLastSyncStatus": {
|
||||||
|
"success": "Success",
|
||||||
|
"error": "Failed",
|
||||||
|
"pending": "Pending"
|
||||||
|
},
|
||||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||||
"proxmoxImportTitle": "Import from Proxmox",
|
"proxmoxImportTitle": "Import from Proxmox",
|
||||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||||
|
|||||||
@@ -864,6 +864,21 @@
|
|||||||
"proxmoxDockerDetectionDesc": "逗号分隔的名称模式使 Docker 能够匹配虚拟机",
|
"proxmoxDockerDetectionDesc": "逗号分隔的名称模式使 Docker 能够匹配虚拟机",
|
||||||
"proxmoxPreferredRanges": "首选 IP 地址范围",
|
"proxmoxPreferredRanges": "首选 IP 地址范围",
|
||||||
"proxmoxPreferredRangesDesc": "当访客拥有多个接口时,用于选择 IP 地址的优先级前缀以逗号分隔",
|
"proxmoxPreferredRangesDesc": "当访客拥有多个接口时,用于选择 IP 地址的优先级前缀以逗号分隔",
|
||||||
|
"proxmoxAutoSync": "自动同步访客",
|
||||||
|
"proxmoxAutoSyncDesc": "在会话解锁时定期发现此 Proxmox 节点,并创建或更新导入的访客主机。",
|
||||||
|
"proxmoxSyncInterval": "同步间隔(分钟)",
|
||||||
|
"proxmoxSyncIntervalDesc": "最小 5 分钟。用户会话锁定时调度器会跳过同步。",
|
||||||
|
"proxmoxMarkMissing": "标记消失的访客",
|
||||||
|
"proxmoxMarkMissingDesc": "以前导入的访客消失时添加 proxmox-missing 标签,而不是删除主机。",
|
||||||
|
"proxmoxLastSync": "上次同步",
|
||||||
|
"proxmoxLastSyncNever": "尚未同步",
|
||||||
|
"proxmoxLastSyncNoResult": "暂无结果详情",
|
||||||
|
"proxmoxLastSyncSummary": "创建 {{created}},更新 {{updated}},消失 {{markedMissing}},跳过 {{skipped}}",
|
||||||
|
"proxmoxLastSyncStatus": {
|
||||||
|
"success": "成功",
|
||||||
|
"error": "失败",
|
||||||
|
"pending": "等待中"
|
||||||
|
},
|
||||||
"proxmoxDiscoverAction": "发现并导入 Proxmox 访客",
|
"proxmoxDiscoverAction": "发现并导入 Proxmox 访客",
|
||||||
"proxmoxImportTitle": "从 Proxmox 导入",
|
"proxmoxImportTitle": "从 Proxmox 导入",
|
||||||
"proxmoxSelectHost": "选择 Proxmox 主机…",
|
"proxmoxSelectHost": "选择 Proxmox 主机…",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@ export {
|
|||||||
bulkImportSSHHosts,
|
bulkImportSSHHosts,
|
||||||
importSSHConfigHosts,
|
importSSHConfigHosts,
|
||||||
discoverProxmoxGuests,
|
discoverProxmoxGuests,
|
||||||
|
syncProxmoxGuests,
|
||||||
bulkUpdateSSHHosts,
|
bulkUpdateSSHHosts,
|
||||||
deleteSSHHost,
|
deleteSSHHost,
|
||||||
getSSHHostById,
|
getSSHHostById,
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ export function createHostEditorForm(
|
|||||||
windowsPatterns: "win, windows",
|
windowsPatterns: "win, windows",
|
||||||
dockerPatterns: "docker",
|
dockerPatterns: "docker",
|
||||||
preferredPrefixes: "10., 192.168.",
|
preferredPrefixes: "10., 192.168.",
|
||||||
|
autoSyncEnabled: false,
|
||||||
|
syncIntervalMinutes: 15,
|
||||||
|
markMissingGuests: true,
|
||||||
},
|
},
|
||||||
enableTunnel: host?.enableTunnel ?? false,
|
enableTunnel: host?.enableTunnel ?? false,
|
||||||
defaultPath: host?.defaultPath ?? "/",
|
defaultPath: host?.defaultPath ?? "/",
|
||||||
|
|||||||
@@ -105,7 +105,22 @@ export function HostProxmoxTab({
|
|||||||
windowsPatterns: "win, windows",
|
windowsPatterns: "win, windows",
|
||||||
dockerPatterns: "docker",
|
dockerPatterns: "docker",
|
||||||
preferredPrefixes: "10., 192.168.",
|
preferredPrefixes: "10., 192.168.",
|
||||||
|
autoSyncEnabled: false,
|
||||||
|
syncIntervalMinutes: 15,
|
||||||
|
markMissingGuests: true,
|
||||||
};
|
};
|
||||||
|
const lastSyncResult = cfg.lastSyncResult;
|
||||||
|
const lastSyncSummary = lastSyncResult
|
||||||
|
? t("hosts.proxmoxLastSyncSummary", {
|
||||||
|
created: lastSyncResult.created,
|
||||||
|
updated: lastSyncResult.updated,
|
||||||
|
markedMissing: lastSyncResult.markedMissing,
|
||||||
|
skipped: lastSyncResult.skipped,
|
||||||
|
})
|
||||||
|
: t("hosts.proxmoxLastSyncNoResult");
|
||||||
|
const lastSyncDescription = cfg.lastSyncAt
|
||||||
|
? `${new Date(cfg.lastSyncAt).toLocaleString()} · ${lastSyncSummary}`
|
||||||
|
: t("hosts.proxmoxLastSyncNever");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
@@ -231,6 +246,71 @@ export function HostProxmoxTab({
|
|||||||
placeholder="10., 192.168."
|
placeholder="10., 192.168."
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
label={t("hosts.proxmoxAutoSync")}
|
||||||
|
description={t("hosts.proxmoxAutoSyncDesc")}
|
||||||
|
>
|
||||||
|
<FakeSwitch
|
||||||
|
checked={cfg.autoSyncEnabled === true}
|
||||||
|
onChange={(v) =>
|
||||||
|
setField("proxmoxConfig", {
|
||||||
|
...cfg,
|
||||||
|
autoSyncEnabled: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
label={t("hosts.proxmoxSyncInterval")}
|
||||||
|
description={t("hosts.proxmoxSyncIntervalDesc")}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className="w-24 h-7 text-xs"
|
||||||
|
type="number"
|
||||||
|
min={5}
|
||||||
|
value={cfg.syncIntervalMinutes ?? 15}
|
||||||
|
onChange={(e) =>
|
||||||
|
setField("proxmoxConfig", {
|
||||||
|
...cfg,
|
||||||
|
syncIntervalMinutes: Math.max(
|
||||||
|
5,
|
||||||
|
Number.parseInt(e.target.value || "15", 10),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
label={t("hosts.proxmoxMarkMissing")}
|
||||||
|
description={t("hosts.proxmoxMarkMissingDesc")}
|
||||||
|
>
|
||||||
|
<FakeSwitch
|
||||||
|
checked={cfg.markMissingGuests !== false}
|
||||||
|
onChange={(v) =>
|
||||||
|
setField("proxmoxConfig", {
|
||||||
|
...cfg,
|
||||||
|
markMissingGuests: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
label={t("hosts.proxmoxLastSync")}
|
||||||
|
description={lastSyncDescription}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`text-xs ${
|
||||||
|
cfg.lastSyncStatus === "error"
|
||||||
|
? "text-destructive"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
title={cfg.lastSyncError ?? undefined}
|
||||||
|
>
|
||||||
|
{cfg.lastSyncStatus
|
||||||
|
? t(`hosts.proxmoxLastSyncStatus.${cfg.lastSyncStatus}`)
|
||||||
|
: t("hosts.proxmoxLastSyncStatus.pending")}
|
||||||
|
</span>
|
||||||
|
</SettingRow>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user