diff --git a/README.md b/README.md index 0393ba10a..71481f803 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A lightweight monitoring application for Proxmox VE that displays real-time stat - [Configuration](#️-configuration) - [Environment Variables](#environment-variables) - [Creating a Proxmox API Token](#creating-a-proxmox-api-token) + - [Creating a Proxmox Backup Server API Token](#creating-a-proxmox-backup-server-api-token) - [Required Permissions](#required-permissions) - [Installation](#-installation) - [Running the Application](#-running-the-application) @@ -62,6 +63,36 @@ A lightweight monitoring application for Proxmox VE that displays real-time stat If you only need to monitor a single Proxmox cluster or node, you only need to set the primary variables (`PROXMOX_HOST`, `PROXMOX_TOKEN_ID`, `PROXMOX_TOKEN_SECRET`). + **Proxmox Backup Server (PBS) Configuration (Optional):** + + Pulse can also monitor backup status information from a Proxmox Backup Server instance. If you want to enable this feature, configure the following environment variables: + + - `PBS_HOST`: URL of your Proxmox Backup Server (e.g., `https://your-pbs-ip-or-hostname:8007`). + - `PBS_TOKEN_ID`: Your PBS API Token ID (e.g., `user@pam!tokenid`). Create this in the PBS UI (see below). + - `PBS_TOKEN_SECRET`: Your PBS API Token Secret. + - `PBS_NODE_NAME`: (Potentially Required) The internal hostname of your PBS server. **Crucially, this might be different from the hostname used in `PBS_HOST`**. See detailed explanation below. + - `PBS_ALLOW_SELF_SIGNED_CERTS`: (Optional) Set to `true` if your PBS server uses self-signed SSL certificates. Defaults to `false`. + - `PBS_PORT`: (Optional) Port for the PBS API. Defaults to `8007`. + + *Note: Currently, Pulse only supports monitoring a single PBS instance. Numbered variables like `PBS_HOST_2` are not yet supported.* + + **Why `PBS_NODE_NAME` is Important:** + + Pulse needs to query task lists specific to the PBS node (e.g., `/api2/json/nodes/{nodeName}/tasks`). While Pulse attempts to discover this node name automatically, this can fail due to API limitations or specific permission issues (like accessing `/api2/json/nodes`, which is not a standard PBS management endpoint). + + If Pulse cannot fetch task data correctly (which might result in recent backups showing as 'stale'), you likely need to set `PBS_NODE_NAME` manually. + + **How to find your PBS Node Name:** + 1. **SSH:** Log into your PBS server via SSH and run the command `hostname`. The output is the value needed for `PBS_NODE_NAME`. + 2. **UI:** Log into the PBS web interface. The hostname is typically displayed on the main Dashboard under Server Status. + 3. **MOTD:** The hostname is often shown in the Message of the Day when you log in via SSH. + + Example: If your PBS connects via `https://minipc-pbs.lan:8007` but its internal hostname is `proxmox-backup-server`, you would set: + ``` + PBS_HOST=https://minipc-pbs.lan:8007 + PBS_NODE_NAME=proxmox-backup-server + ``` + ### Creating a Proxmox API Token An API token is recommended for connecting Pulse to Proxmox. @@ -103,6 +134,34 @@ An API token is recommended for connecting Pulse to Proxmox. 5. **Update your `server/.env` file** with the `Token ID` (which looks like `user@realm!tokenid`, e.g., `pulse-monitor@pam!pulse`) and the `Secret` you saved. +### Creating a Proxmox Backup Server API Token + +If you are configuring PBS monitoring, you need a separate API token created within PBS. + +1. **Log in to the Proxmox Backup Server web interface** +2. **Create a dedicated user** (optional but recommended) + * Go to `Configuration` → `Access Control` → `User Management`. + * Click `Add`. + * Enter a `User ID` (e.g., "pulse-monitor@pam"), set other fields as needed, and click `Add`. +3. **Create an API token** + * Under `Configuration` → `Access Control`, select `API Token`. + * Click `Add`. + * Select the `User` (e.g., "pulse-monitor@pam") or `root@pam`. + * Enter a `Token Name` (e.g., "pulse"). + * Leave `Privilege Separation` checked. + * Click `Add`. + * **Important:** Copy the displayed `Secret` value immediately. +4. **Assign permissions** + * Under `Configuration` → `Access Control`, select `Permissions`. + * Click `Add` → `API Token Permission`. + * Path: `/datastore` (This grants access to view datastores and their contents, including backup snapshots). + * API Token: Select the token you created (e.g., "pulse-monitor@pam!pulse"). + * Role: `DatastoreAudit` (Provides read-only access to datastore contents and backups). + * Ensure `Propagate` is checked. + * Click `Add`. + * *(Note: While `DatastoreAudit` on `/datastore` is usually sufficient for backup status, if you still encounter issues fetching task lists, you might need broader permissions like `Audit` on path `/` for the token, although this has shown inconsistent behaviour with API tokens vs user sessions in some PBS versions).* +5. **Update your `server/.env` file** with the PBS `Token ID` (`PBS_TOKEN_ID`) and the `Secret` (`PBS_TOKEN_SECRET`). + ### Required Permissions The `PVEAuditor` role is recommended as it provides the necessary read-only permissions for Pulse to monitor your Proxmox environment: diff --git a/server/index.js b/server/index.js index 2e4d4b0a7..e2762faa7 100644 --- a/server/index.js +++ b/server/index.js @@ -301,7 +301,7 @@ async function initializeAllPbsClients() { }); clientData = { client: pbsAxiosInstance, config: config }; - console.log(`INFO: [PBS Init] Successfully initialized client for instance '${config.name}' (Token Auth)`); + console.log(`INFO: Successfully initialized client for instance '${config.name}' (Token Auth)`); } else { // This case should not be reachable anymore if loadPbsConfig only creates 'token' authMethod configs console.error(`ERROR: Unexpected authMethod '${config.authMethod}' found during PBS client initialization for: ${config.name}`); @@ -311,12 +311,12 @@ async function initializeAllPbsClients() { pbsApiClients[config.id] = clientData; // Store successful client keyed by config ID } } catch (error) { - console.error(`ERROR: Unhandled exception during PBS client initialization for ${config.name}: ${error.message}`); + console.error(`ERROR: Unhandled exception during PBS client initialization for ${config.name}: ${error.message}`, error.stack); } }); await Promise.allSettled(initPromises); - console.log(`INFO: [PBS Init] Finished initialization. ${Object.keys(pbsApiClients).length} / ${pbsConfigs.length} PBS clients initialized successfully.`); + console.log(`INFO: Finished initialization. ${Object.keys(pbsApiClients).length} / ${pbsConfigs.length} PBS clients initialized successfully.`); } if (Object.keys(apiClients).length === 0 && pbsConfigs.length === 0) { @@ -1205,7 +1205,7 @@ async function fetchPbsNodeName(pbsClient) { return 'localhost'; // Fallback } } catch (error) { - console.error(`ERROR: Failed to fetch PBS nodes list: ${error.message}`); + console.error(`ERROR: Failed to fetch PBS nodes list for ${pbsClient.config.name}: ${error.message}`, error.stack); return 'localhost'; // Fallback on error } } @@ -1225,21 +1225,22 @@ async function fetchAllPbsTasksForProcessing(pbsClient, nodeName) { return { tasks: null, error: true }; } try { - const sinceTimestamp = Math.floor((Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000); - const response = await pbsClient.client.get(`/nodes/${nodeName}/tasks`, { - params: { - since: sinceTimestamp, - limit: 1000, // Fetch a larger number to cover 7 days of various tasks - errors: 1, - } - }); + const sinceTimestamp = Math.floor((Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000); // RESTORED + const requestUrl = `/nodes/${nodeName}/tasks`; + const requestParams = { + since: sinceTimestamp, // RESTORED + limit: 1000, // Kept (was 1000 after increasing from 20) + errors: 1, // RESTORED + }; + const paramsToSend = Object.keys(requestParams).length > 0 ? { params: requestParams } : {}; + const response = await pbsClient.client.get(requestUrl, paramsToSend); const allTasks = response.data?.data ?? []; - console.log(`INFO: Fetched ${allTasks.length} tasks from PBS for processing.`); return { tasks: allTasks, error: false }; } catch (error) { - console.error(`ERROR: Failed to fetch PBS task list for node ${nodeName}: ${error.message}`); - if (error.response?.status === 401) console.error("ERROR: PBS API authentication failed (401)."); - else if (error.response?.status === 403) console.error("ERROR: PBS API authorization failed (403)."); + console.error(`ERROR: Failed to fetch PBS task list for node ${nodeName} (${pbsClient.config.name}): ${error.message}`, error.stack); + if (error.response) { // Log more detail if available + console.error(`Error details: Status=${error.response.status}, Data=${JSON.stringify(error.response.data)}`); + } return { tasks: null, error: true }; } } @@ -1305,10 +1306,10 @@ function processPbsTasks(allTasks) { // Process and sort recent tasks for each category const sortTasksDesc = (a, b) => (b.startTime || 0) - (a.startTime || 0); - const recentBackupTasks = taskResults.backup.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 20); - const recentVerifyTasks = taskResults.verify.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 20); - const recentSyncTasks = taskResults.sync.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 20); - const recentPruneGcTasks = taskResults.pruneGc.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 20); + const recentBackupTasks = taskResults.backup.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 100); + const recentVerifyTasks = taskResults.verify.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 100); + const recentSyncTasks = taskResults.sync.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 100); + const recentPruneGcTasks = taskResults.pruneGc.list.map(createDetailedTask).sort(sortTasksDesc).slice(0, 100); console.log(`INFO: Processed PBS Tasks - Backup: ${taskResults.backup.list.length} (OK: ${taskResults.backup.ok}, Fail: ${taskResults.backup.failed}), Verify: ${taskResults.verify.list.length} (OK: ${taskResults.verify.ok}, Fail: ${taskResults.verify.failed}), Sync: ${taskResults.sync.list.length} (OK: ${taskResults.sync.ok}, Fail: ${taskResults.sync.failed}), Prune/GC: ${taskResults.pruneGc.list.length} (OK: ${taskResults.pruneGc.ok}, Fail: ${taskResults.pruneGc.failed})`); @@ -1366,11 +1367,13 @@ async function fetchPbsTaskSummaryByType(pbsClient, nodeName, taskTypes) { async function fetchPbsDatastoreData(pbsClient) { // Fetches datastore usage details from PBS using the /status/datastore-usage endpoint - console.log("INFO: Fetching PBS datastore data..."); + console.log(`INFO: Fetching PBS datastore data for ${pbsClient.config.name}...`); let datastores = []; + const primaryUrl = '/status/datastore-usage'; + const fallbackUrl = '/config/datastore'; try { // Fetch usage stats for all datastores at once - const usageResponse = await pbsClient.client.get('/status/datastore-usage'); + const usageResponse = await pbsClient.client.get(primaryUrl); const usageData = usageResponse.data?.data ?? []; if (usageData.length > 0) { @@ -1391,14 +1394,15 @@ async function fetchPbsDatastoreData(pbsClient) { } } catch (usageError) { - console.error(`ERROR: Failed to fetch PBS datastore usage via /status/datastore-usage: ${usageError.message}. Trying fallback /config/datastore.`); + console.error(`ERROR: Failed to fetch PBS datastore usage via ${primaryUrl} for ${pbsClient.config.name}: ${usageError.message}. Trying fallback ${fallbackUrl}.`, usageError.stack); // --- Fallback Logic --- try { - const configResponse = await pbsClient.client.get('/config/datastore'); + const configResponse = await pbsClient.client.get(fallbackUrl); const datastoresConfig = configResponse.data?.data ?? []; if (datastoresConfig.length > 0) { console.log(`INFO: Fetched config for ${datastoresConfig.length} PBS datastores (fallback). Status unavailable.`); - datastores = datastoresConfig.map(dsConfig => ({ + // Map the received data to the expected format + datastores = datastoresConfig.map(dsConfig => ({ name: dsConfig.name, path: dsConfig.path, total: null, // Usage/Status info unavailable from config @@ -1410,13 +1414,12 @@ async function fetchPbsDatastoreData(pbsClient) { console.warn("WARN: Fallback fetch of PBS datastore config also returned empty data."); } } catch (configError) { - console.error(`ERROR: Fallback fetch of PBS datastore config failed: ${configError.message}`); - if (configError.response?.status === 401 || configError.response?.status === 403) { - console.error("ERROR: PBS API authentication/authorization failed for datastore config access."); + console.error(`ERROR: Fallback fetch of PBS datastore config (${fallbackUrl}) for ${pbsClient.config.name}: ${configError.message}`, configError.stack); + if (configError.response) { // Log more detail if available + console.error(`Fallback error details: Status=${configError.response.status}, Data=${JSON.stringify(configError.response.data)}`); } // Keep datastores as empty array if both primary and fallback attempts fail } - // --- End Fallback --- } console.log(`INFO: Finished fetching PBS datastore data. Found ${datastores.length} datastores.`);