feat(widget): add proxmox integration (#1969)

* feat(widget): add proxmox integration

* fix: broken lock file

* fix: ci issues

* fix: ci issues

* fix: ci issues

* chore: debug temporary

* fix: name is not used correctly for nodes and storage in proxmox

* fix: remove temporary debu logs

* fix: job runs for both cluster and system health and throws error

* fix: ts-expect-error is unnecessary

* fix: remove unused import
This commit is contained in:
Meier Lukas
2025-01-17 13:01:04 +01:00
committed by GitHub
parent a31c6a97e0
commit 3ed46aecbd
22 changed files with 1325 additions and 426 deletions

View File

@@ -23,6 +23,7 @@ import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
import { PlexIntegration } from "../plex/plex-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import type { Integration, IntegrationInput } from "./integration";
export const integrationCreator = <TKind extends keyof typeof integrationCreators>(
@@ -72,4 +73,5 @@ export const integrationCreators = {
readarr: ReadarrIntegration,
dashDot: DashDotIntegration,
tdarr: TdarrIntegration,
proxmox: ProxmoxIntegration,
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;

View File

@@ -0,0 +1,125 @@
import type { Proxmox } from "proxmox-api";
import proxmoxApi from "proxmox-api";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { extractErrorMessage } from "@homarr/common";
import { logger } from "@homarr/log";
import { Integration } from "../base/integration";
import { IntegrationTestConnectionError } from "../base/test-connection-error";
import type {
ComputeResourceBase,
LxcResource,
NodeResource,
QemuResource,
Resource,
StorageResource,
} from "./proxmox-types";
export class ProxmoxIntegration extends Integration {
public async testConnectionAsync(): Promise<void> {
const proxmox = this.getPromoxApi();
await proxmox.nodes.$get().catch((error) => {
throw new IntegrationTestConnectionError("internalServerError", extractErrorMessage(error));
});
}
public async getClusterInfoAsync() {
const proxmox = this.getPromoxApi();
const resources = await proxmox.cluster.resources.$get();
logger.info(
`Found ${resources.length} resources in Proxmox cluster node=${resources.filter((resource) => resource.type === "node").length} lxc=${resources.filter((resource) => resource.type === "lxc").length} qemu=${resources.filter((resource) => resource.type === "qemu").length} storage=${resources.filter((resource) => resource.type === "storage").length}`,
);
const mappedResources = resources.map(mapResource).filter((resource) => resource !== null);
return {
nodes: mappedResources.filter((resource): resource is NodeResource => resource.type === "node"),
lxcs: mappedResources.filter((resource): resource is LxcResource => resource.type === "lxc"),
vms: mappedResources.filter((resource): resource is QemuResource => resource.type === "qemu"),
storages: mappedResources.filter((resource): resource is StorageResource => resource.type === "storage"),
};
}
private getPromoxApi() {
return proxmoxApi({
host: this.url("/").host,
tokenID: `${this.getSecretValue("username")}@${this.getSecretValue("realm")}!${this.getSecretValue("tokenId")}`,
tokenSecret: this.getSecretValue("apiKey"),
fetch: fetchWithTrustedCertificatesAsync,
});
}
}
const mapResource = (resource: Proxmox.clusterResourcesResources): Resource | null => {
switch (resource.type) {
case "node":
return mapNodeResource(resource);
case "lxc":
case "qemu":
return mapVmResource(resource);
case "storage":
return mapStorageResource(resource);
}
return null;
};
const mapComputeResource = (resource: Proxmox.clusterResourcesResources): Omit<ComputeResourceBase<string>, "type"> => {
return {
cpu: {
utilization: resource.cpu ?? 0,
cores: resource.maxcpu ?? 0,
},
memory: {
used: resource.mem ?? 0,
total: resource.maxmem ?? 0,
},
storage: {
used: resource.disk ?? 0,
total: resource.maxdisk ?? 0,
read: (resource.diskread as number | null) ?? null,
write: (resource.diskwrite as number | null) ?? null,
},
network: {
in: (resource.netin as number | null) ?? null,
out: (resource.netout as number | null) ?? null,
},
haState: resource.hastate ?? null,
isRunning: resource.status === "running" || resource.status === "online",
name: resource.name ?? "",
node: resource.node ?? "",
status: resource.status ?? (resource.type === "node" ? "offline" : "stopped"),
uptime: resource.uptime ?? 0,
};
};
const mapNodeResource = (resource: Proxmox.clusterResourcesResources): NodeResource => {
return {
type: "node",
...mapComputeResource(resource),
name: resource.node ?? "",
};
};
const mapVmResource = (resource: Proxmox.clusterResourcesResources): LxcResource | QemuResource => {
return {
type: resource.type as "lxc" | "qemu",
vmId: resource.vmid ?? 0,
...mapComputeResource(resource),
};
};
const mapStorageResource = (resource: Proxmox.clusterResourcesResources): StorageResource => {
return {
type: "storage",
name: resource.storage ?? "",
node: resource.node ?? "",
isRunning: resource.status === "available",
status: resource.status ?? "offline",
storagePlugin: resource.storage ?? "",
total: resource.maxdisk ?? 0,
used: resource.disk ?? 0,
isShared: resource.shared === 1,
};
};

View File

@@ -0,0 +1,57 @@
interface ResourceBase<TType extends string> {
type: TType;
name: string;
node: string;
isRunning: boolean;
status: string;
}
export interface ComputeResourceBase<TType extends string> extends ResourceBase<TType> {
cpu: {
utilization: number; // previously cpu (0-1)
cores: number; // previously cpuCores
};
memory: {
used: number; // previously mem
total: number; // previously maxMem
};
storage: {
used: number; // previously disk
total: number; // previously maxDisk
read: number | null; // previously diskRead
write: number | null; // previously diskWrite
};
network: {
in: number | null; // previously netIn
out: number | null; // previously netOut
};
uptime: number; // expressed in seconds
haState: string | null; // HA service status (for HA managed VMs).
}
export type NodeResource = ComputeResourceBase<"node">;
export interface LxcResource extends ComputeResourceBase<"lxc"> {
vmId: number;
}
export interface QemuResource extends ComputeResourceBase<"qemu"> {
vmId: number;
}
export interface StorageResource extends ResourceBase<"storage"> {
storagePlugin: string;
used: number; // previously disk
total: number; // previously maxDisk
isShared: boolean; // previously storageShared
}
export type ComputeResource = NodeResource | LxcResource | QemuResource;
export type Resource = ComputeResource | StorageResource;
export interface ProxmoxClusterInfo {
nodes: NodeResource[];
lxcs: LxcResource[];
vms: QemuResource[];
storages: StorageResource[];
}

View File

@@ -6,3 +6,4 @@ export * from "./interfaces/media-requests/media-request";
export * from "./pi-hole/pi-hole-types";
export * from "./base/searchable-integration";
export * from "./homeassistant/homeassistant-types";
export * from "./proxmox/proxmox-types";