mirror of
https://github.com/gethomepage/homepage.git
synced 2025-07-09 03:04:18 -04:00
Feature: Proxmox status & stats integration (#5385)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
parent
30abf4e422
commit
5759596a37
79
docs/configs/proxmox.md
Normal file
79
docs/configs/proxmox.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: Proxmox
|
||||||
|
description: Proxmox Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: https://proxmox.host.or.ip:8006
|
||||||
|
token: username@pam!Token ID
|
||||||
|
secret: secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
Once the Proxmox connection is configured, individual services can be configured to pull statistics of VMs or LXCs. Only CPU and Memory are currently supported.
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
- `proxmoxNode`: The name of the Proxmox node where your VM/LXC is running
|
||||||
|
- `proxmoxVMID`: The ID of the Proxmox VM or LXC container
|
||||||
|
- `proxmoxType`: (Optional) The type of Proxmox virtual machine. Defaults to `qemu` for VMs, but can be set to `lxc` for LXC containers
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
For a QEMU VM (default):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- HomeAssistant:
|
||||||
|
icon: home-assistant.png
|
||||||
|
href: http://homeassistant.local/
|
||||||
|
description: Home automation
|
||||||
|
proxmoxNode: pve
|
||||||
|
proxmoxVMID: 101
|
||||||
|
# proxmoxType: qemu # This is the default, so it can be omitted
|
||||||
|
```
|
||||||
|
|
||||||
|
For an LXC container:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- Nginx:
|
||||||
|
icon: nginx.png
|
||||||
|
href: http://nginx.local/
|
||||||
|
description: Web server
|
||||||
|
proxmoxNode: pve
|
||||||
|
proxmoxVMID: 200
|
||||||
|
proxmoxType: lxc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create token
|
||||||
|
|
||||||
|
You will need to generate an API Token for new or an existing user. Here is an example of how to do this for a new user.
|
||||||
|
|
||||||
|
1. Navigate to the Proxmox portal, click on Datacenter
|
||||||
|
2. Expand Permissions, click on Groups
|
||||||
|
3. Click the Create button
|
||||||
|
4. Name the group something informative, like api-ro-users
|
||||||
|
5. Click on the Permissions "folder"
|
||||||
|
6. Click Add -> Group Permission
|
||||||
|
- Path: /
|
||||||
|
- Group: group from bullet 4 above
|
||||||
|
- Role: PVEAuditor
|
||||||
|
- Propagate: Checked
|
||||||
|
7. Expand Permissions, click on Users
|
||||||
|
8. Click the Add button
|
||||||
|
- User name: something informative like `api`
|
||||||
|
- Realm: Linux PAM standard authentication
|
||||||
|
- Group: group from bullet 4 above
|
||||||
|
9. Expand Permissions, click on API Tokens
|
||||||
|
10. Click the Add button
|
||||||
|
- User: user from bullet 8 above
|
||||||
|
- Token ID: something informative like the application or purpose like `homepage`
|
||||||
|
- Privilege Separation: Checked
|
||||||
|
11. Go back to the "Permissions" menu
|
||||||
|
12. Click Add -> API Token Permission
|
||||||
|
- Path: /
|
||||||
|
- API Token: select the Token ID created in Step 10
|
||||||
|
- Role: PVE Auditor
|
||||||
|
- Propagate: Checked
|
@ -7,34 +7,7 @@ Learn more about [Proxmox](https://www.proxmox.com/en/).
|
|||||||
|
|
||||||
This widget shows the running and total counts of both QEMU VMs and LX Containers in the Proxmox cluster. It also shows the CPU and memory usage of the first node in the cluster.
|
This widget shows the running and total counts of both QEMU VMs and LX Containers in the Proxmox cluster. It also shows the CPU and memory usage of the first node in the cluster.
|
||||||
|
|
||||||
You will need to generate an API Token for new or an existing user. Here is an example of how to do this for a new user.
|
See the [Proxmox configuration documentation](../../configs/proxmox.md#create-token) for details on creating API tokens.
|
||||||
|
|
||||||
1. Navigate to the Proxmox portal, click on Datacenter
|
|
||||||
2. Expand Permissions, click on Groups
|
|
||||||
3. Click the Create button
|
|
||||||
4. Name the group something informative, like api-ro-users
|
|
||||||
5. Click on the Permissions "folder"
|
|
||||||
6. Click Add -> Group Permission
|
|
||||||
- Path: /
|
|
||||||
- Group: group from bullet 4 above
|
|
||||||
- Role: PVEAuditor
|
|
||||||
- Propagate: Checked
|
|
||||||
7. Expand Permissions, click on Users
|
|
||||||
8. Click the Add button
|
|
||||||
- User name: something informative like `api`
|
|
||||||
- Realm: Linux PAM standard authentication
|
|
||||||
- Group: group from bullet 4 above
|
|
||||||
9. Expand Permissions, click on API Tokens
|
|
||||||
10. Click the Add button
|
|
||||||
- User: user from bullet 8 above
|
|
||||||
- Token ID: something informative like the application or purpose like `homepage`
|
|
||||||
- Privilege Separation: Checked
|
|
||||||
11. Go back to the "Permissions" menu
|
|
||||||
12. Click Add -> API Token Permission
|
|
||||||
- Path: /
|
|
||||||
- API Token: select the Token ID created in Step 10
|
|
||||||
- Role: PVE Auditor
|
|
||||||
- Propagate: Checked
|
|
||||||
|
|
||||||
Use `username@pam!Token ID` as the `username` (e.g `api@pam!homepage`) setting and `Secret` as the `password` setting.
|
Use `username@pam!Token ID` as the `username` (e.g `api@pam!homepage`) setting and `Secret` as the `password` setting.
|
||||||
|
|
||||||
|
@ -4,9 +4,11 @@ import { useContext, useState } from "react";
|
|||||||
import { SettingsContext } from "utils/contexts/settings";
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
import Docker from "widgets/docker/component";
|
import Docker from "widgets/docker/component";
|
||||||
import Kubernetes from "widgets/kubernetes/component";
|
import Kubernetes from "widgets/kubernetes/component";
|
||||||
|
import ProxmoxVM from "widgets/proxmoxvm/component";
|
||||||
|
|
||||||
import KubernetesStatus from "./kubernetes-status";
|
import KubernetesStatus from "./kubernetes-status";
|
||||||
import Ping from "./ping";
|
import Ping from "./ping";
|
||||||
|
import ProxmoxStatus from "./proxmox-status";
|
||||||
import SiteMonitor from "./site-monitor";
|
import SiteMonitor from "./site-monitor";
|
||||||
import Status from "./status";
|
import Status from "./status";
|
||||||
import Widget from "./widget";
|
import Widget from "./widget";
|
||||||
@ -121,6 +123,16 @@ export default function Item({ service, groupName, useEqualHeights }) {
|
|||||||
<span className="sr-only">View container stats</span>
|
<span className="sr-only">View container stats</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{service.proxmoxNode && service.proxmoxVMID && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
|
||||||
|
className="shrink-0 flex items-center justify-center cursor-pointer service-tag service-proxmoxstatus"
|
||||||
|
>
|
||||||
|
<ProxmoxStatus service={service} style={statusStyle} />
|
||||||
|
<span className="sr-only">View Proxmox stats</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -152,6 +164,26 @@ export default function Item({ service, groupName, useEqualHeights }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{service.proxmoxNode && service.proxmoxVMID && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
showStats || (statsOpen && !statsClosing) ? "max-h-[110px] opacity-100" : " max-h-0 opacity-0",
|
||||||
|
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(showStats || statsOpen) && (
|
||||||
|
<ProxmoxVM
|
||||||
|
service={{
|
||||||
|
widget: {
|
||||||
|
node: service.proxmoxNode,
|
||||||
|
vmid: service.proxmoxVMID,
|
||||||
|
type: service.proxmoxType,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{service.widgets.map((widget) => (
|
{service.widgets.map((widget) => (
|
||||||
<Widget widget={widget} service={service} key={widget.index} />
|
<Widget widget={widget} service={service} key={widget.index} />
|
||||||
|
65
src/components/services/proxmox-status.jsx
Normal file
65
src/components/services/proxmox-status.jsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export default function ProxmoxStatus({ service, style }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const vmType = service.proxmoxType || "qemu";
|
||||||
|
const apiUrl = `/api/proxmox/stats/${service.proxmoxNode}/${service.proxmoxVMID}?type=${vmType}`;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(apiUrl);
|
||||||
|
|
||||||
|
let statusLabel = t("docker.unknown");
|
||||||
|
let backgroundClass = "px-1.5 py-0.5 bg-theme-500/10 dark:bg-theme-900/50";
|
||||||
|
let colorClass = "text-black/20 dark:text-white/40 ";
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
statusLabel = t("docker.error");
|
||||||
|
colorClass = "text-rose-500/80";
|
||||||
|
} else if (data) {
|
||||||
|
if (data.status === "running") {
|
||||||
|
statusLabel = t("docker.running");
|
||||||
|
colorClass = "text-emerald-500/80";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === "stopped") {
|
||||||
|
statusLabel = t("docker.exited");
|
||||||
|
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === "paused") {
|
||||||
|
statusLabel = "paused";
|
||||||
|
colorClass = "text-blue-500/80";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === "offline") {
|
||||||
|
statusLabel = "offline";
|
||||||
|
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === "not found") {
|
||||||
|
statusLabel = t("docker.not_found");
|
||||||
|
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === "dot") {
|
||||||
|
colorClass = colorClass.replace(/text-/g, "bg-").replace(/\/\d\d/g, "");
|
||||||
|
backgroundClass = "p-4 hover:bg-theme-500/10 dark:hover:bg-theme-900/20";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-auto text-center overflow-hidden ${backgroundClass} rounded-b-[3px] proxmoxstatus proxmoxstatus-${statusLabel
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(" ", "-")}`}
|
||||||
|
title={statusLabel}
|
||||||
|
>
|
||||||
|
{style !== "dot" ? (
|
||||||
|
<div className={`text-[8px] font-bold ${colorClass} uppercase`}>{statusLabel}</div>
|
||||||
|
) : (
|
||||||
|
<div className={`rounded-full h-3 w-3 ${colorClass}`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
65
src/pages/api/proxmox/stats/[...service].js
Normal file
65
src/pages/api/proxmox/stats/[...service].js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { getProxmoxConfig } from "utils/config/proxmox";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
|
||||||
|
const logger = createLogger("proxmoxStatsService");
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const { service, type: vmType } = req.query;
|
||||||
|
|
||||||
|
const [node, vmid] = service;
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return res.status(400).send({
|
||||||
|
error: "Proxmox node parameter is required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proxmoxConfig = getProxmoxConfig();
|
||||||
|
|
||||||
|
if (!proxmoxConfig) {
|
||||||
|
return res.status(500).send({
|
||||||
|
error: "Proxmox server configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = `${proxmoxConfig.url}/api2/json`;
|
||||||
|
const headers = {
|
||||||
|
Authorization: `PVEAPIToken=${proxmoxConfig.token}=${proxmoxConfig.secret}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusUrl = `${baseUrl}/nodes/${node}/${vmType}/${vmid}/status/current`;
|
||||||
|
|
||||||
|
const [status, , data] = await httpProxy(statusUrl, {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error("HTTP Error %d calling Proxmox API", status);
|
||||||
|
return res.status(status).send({
|
||||||
|
error: `Failed to fetch Proxmox ${vmType} status`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedData = JSON.parse(Buffer.from(data).toString());
|
||||||
|
|
||||||
|
if (!parsedData || !parsedData.data) {
|
||||||
|
return res.status(500).send({
|
||||||
|
error: "Invalid response from Proxmox API",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
status: parsedData.data.status || "unknown",
|
||||||
|
cpu: parsedData.data.cpu,
|
||||||
|
mem: parsedData.data.mem,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching Proxmox status:", error);
|
||||||
|
return res.status(500).send({
|
||||||
|
error: "Failed to fetch Proxmox status",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import checkAndCopyConfig from "utils/config/config";
|
import checkAndCopyConfig from "utils/config/config";
|
||||||
|
|
||||||
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "kubernetes.yaml"];
|
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "kubernetes.yaml", "proxmox.yaml"];
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
|
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
|
||||||
|
4
src/skeleton/proxmox.yaml
Normal file
4
src/skeleton/proxmox.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
# url: https://proxmox.host.or.ip:8006
|
||||||
|
# token: username@pam!Token ID
|
||||||
|
# secret: secret
|
14
src/utils/config/proxmox.js
Normal file
14
src/utils/config/proxmox.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { readFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
|
||||||
|
|
||||||
|
export function getProxmoxConfig() {
|
||||||
|
checkAndCopyConfig("proxmox.yaml");
|
||||||
|
const configFile = path.join(CONF_DIR, "proxmox.yaml");
|
||||||
|
const rawConfigData = readFileSync(configFile, "utf8");
|
||||||
|
const configData = substituteEnvironmentVars(rawConfigData);
|
||||||
|
return yaml.load(configData);
|
||||||
|
}
|
32
src/widgets/proxmoxvm/component.jsx
Normal file
32
src/widgets/proxmoxvm/component.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export default function ProxmoxVM({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(`/api/proxmox/stats/${widget.node}/${widget.vmid}?type=${widget.type || "qemu"}`);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Container service={service} error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="resources.cpu" />
|
||||||
|
<Block label="resources.mem" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="resources.cpu" value={t("common.percent", { value: data.cpu })} />
|
||||||
|
<Block label="resources.mem" value={t("common.bytes", { value: data.mem })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user