2026-06-19 10:28:37 +00:00
|
|
|
import { Agent } from 'undici'
|
2026-06-18 20:12:40 +00:00
|
|
|
import type { IntegrationAdapter, Resource } from './types.js'
|
|
|
|
|
|
|
|
|
|
interface ProxmoxResourceEntry {
|
|
|
|
|
type: string
|
|
|
|
|
name?: string
|
|
|
|
|
status?: string
|
|
|
|
|
vmid?: number
|
|
|
|
|
node?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function authHeader(apiKey: string): Record<string, string> {
|
|
|
|
|
return { Authorization: `PVEAPIToken=${apiKey}` }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 10:28:37 +00:00
|
|
|
// Proxmox ships with a self-signed cert by default, which Node's fetch rejects out of the box.
|
|
|
|
|
const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } })
|
|
|
|
|
|
2026-06-20 10:31:11 +00:00
|
|
|
// undici's fetch() wraps the real failure (DNS, TLS, connection refused/timed out) in a generic
|
|
|
|
|
// "fetch failed" TypeError — unwrap err.cause so the actual reason reaches the UI.
|
|
|
|
|
function describeFetchError(err: unknown): string {
|
|
|
|
|
if (err instanceof Error) {
|
|
|
|
|
const cause = (err as { cause?: unknown }).cause
|
|
|
|
|
if (cause instanceof Error) return `${err.message}: ${cause.message}`
|
|
|
|
|
return err.message
|
|
|
|
|
}
|
|
|
|
|
return 'Connection failed'
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 20:12:40 +00:00
|
|
|
export const proxmox: IntegrationAdapter = {
|
|
|
|
|
async testConnection(config, secrets) {
|
|
|
|
|
const baseUrl = config.baseUrl?.replace(/\/$/, '')
|
|
|
|
|
const apiKey = secrets.apiKey
|
|
|
|
|
if (!baseUrl) return { ok: false, message: 'Missing baseUrl' }
|
|
|
|
|
if (!apiKey) return { ok: false, message: 'Missing API token' }
|
|
|
|
|
try {
|
2026-06-19 10:28:37 +00:00
|
|
|
const res = await fetch(`${baseUrl}/api2/json/version`, {
|
|
|
|
|
headers: authHeader(apiKey),
|
|
|
|
|
dispatcher: insecureDispatcher,
|
|
|
|
|
} as RequestInit)
|
2026-06-18 20:12:40 +00:00
|
|
|
if (!res.ok) return { ok: false, message: `HTTP ${res.status}` }
|
|
|
|
|
return { ok: true, message: 'Connected' }
|
|
|
|
|
} catch (err) {
|
2026-06-20 10:31:11 +00:00
|
|
|
return { ok: false, message: describeFetchError(err) }
|
2026-06-18 20:12:40 +00:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async listResources(config, secrets): Promise<Resource[]> {
|
|
|
|
|
const baseUrl = config.baseUrl?.replace(/\/$/, '')
|
|
|
|
|
const apiKey = secrets.apiKey
|
|
|
|
|
if (!baseUrl || !apiKey) return []
|
2026-06-19 10:28:37 +00:00
|
|
|
const res = await fetch(`${baseUrl}/api2/json/cluster/resources?type=vm`, {
|
|
|
|
|
headers: authHeader(apiKey),
|
|
|
|
|
dispatcher: insecureDispatcher,
|
|
|
|
|
} as RequestInit)
|
2026-06-18 20:12:40 +00:00
|
|
|
if (!res.ok) return []
|
|
|
|
|
const body = (await res.json()) as { data: ProxmoxResourceEntry[] }
|
|
|
|
|
return body.data.map((entry) => ({
|
|
|
|
|
name: entry.name ?? `vm-${entry.vmid}`,
|
|
|
|
|
status: entry.status === 'running' ? 'healthy' : entry.status === 'stopped' ? 'unknown' : 'warning',
|
|
|
|
|
detail: `${entry.type} on ${entry.node} — ${entry.status}`,
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
}
|