diff --git a/.env.example b/.env.example index 38b0d4d8b..caa95df19 100644 --- a/.env.example +++ b/.env.example @@ -1,98 +1,50 @@ -# Example .env file for Pulse configuration -# Copy this file to .env and replace placeholder values with your actual credentials. +# Pulse Configuration Example +# ---------------------------- -# --- Proxmox VE Endpoint 1 (Required) --- -# Connection details for your primary Proxmox VE host or cluster endpoint. +# --- Proxmox VE Primary Endpoint (Required) --- +# Only API Token authentication is supported. PROXMOX_HOST=your-proxmox-ip-or-hostname -PROXMOX_PORT=8006 # Optional, defaults to 8006 if omitted - -# Authentication: Use *either* API Token (Recommended) *or* User/Password. -# API Token (Recommended): -PROXMOX_TOKEN_ID=your-api-token-id@pam!your-token-name # e.g., root@pam!pulse -PROXMOX_TOKEN_SECRET=your-api-token-secret-uuid # The Secret UUID value - -# User/Password (Less Recommended): -# PROXMOX_USERNAME=your-proxmox-username # e.g., root -# PROXMOX_PASSWORD=your-password -# PROXMOX_REALM=pam # Optional, defaults to 'pam' if omitted (e.g., pam, pve, ad) - -# Optional: Set a friendly name for this endpoint in logs/UI (defaults to host) -PROXMOX_NODE_NAME=My Primary Proxmox - -# Optional: Allow self-signed certificates (defaults to true if omitted) -# Set to false if you have a valid certificate from a trusted CA. -PROXMOX_ALLOW_SELF_SIGNED_CERTS=true - +PROXMOX_TOKEN_ID=your-api-token-id@pam!your-token-name +PROXMOX_TOKEN_SECRET=your-api-token-secret-uuid +# Optional: Specify custom port if not default 8006 +# PROXMOX_PORT=8006 +# Optional: Provide a display name for this endpoint in the UI +# PROXMOX_NODE_NAME=My Primary Proxmox +# Optional: Set to true to allow self-signed certificates (default: true) +# PROXMOX_ALLOW_SELF_SIGNED_CERTS=true # Optional: Enable/Disable this endpoint (defaults to true if omitted) # PROXMOX_ENABLED=true - -# --- Proxmox VE Endpoint 2 (Optional) --- -# Duplicate these variables with a _2 suffix for a second PVE endpoint. -# PROXMOX_HOST_2=second-proxmox-ip-or-hostname +# --- Proxmox VE Additional Endpoints (Optional) --- +# Use consecutive numbers (_2, _3, ...) for additional PVE instances +# Only API Token authentication is supported. +# PROXMOX_HOST_2=second-proxmox-ip +# PROXMOX_TOKEN_ID_2=second-token-id@pve!my-token +# PROXMOX_TOKEN_SECRET_2=second-token-secret # PROXMOX_PORT_2=8006 -# PROXMOX_TOKEN_ID_2=token-id@pam!token-name_2 -# PROXMOX_TOKEN_SECRET_2=token-secret-uuid_2 -# PROXMOX_NODE_NAME_2=My Second Proxmox +# PROXMOX_NODE_NAME_2=My Secondary Proxmox # PROXMOX_ALLOW_SELF_SIGNED_CERTS_2=true # PROXMOX_ENABLED_2=true -# ... User/Password_2 if needed ... +# --- Proxmox Backup Server (PBS) Integration (Optional) --- +# Only API Token authentication is supported. +# Use consecutive numbers (_2, _3, ...) for additional PBS instances +# PBS_HOST=your-pbs-ip-or-hostname +# PBS_TOKEN_ID=your-pbs-token-id@pbs!my-token +# PBS_TOKEN_SECRET=your-pbs-token-secret +# Optional: Specify custom port if not default 8007 +# PBS_PORT=8007 +# Optional: Provide a display name for this PBS instance +# PBS_NODE_NAME=My Primary PBS +# Optional: Set to true to allow self-signed certificates (default: true) +# PBS_ALLOW_SELF_SIGNED_CERTS=true -# --- Proxmox VE Endpoint 3, 4, ... (Optional) --- -# Continue the pattern with _3, _4, etc. for more endpoints. -# PROXMOX_HOST_3=... -# PROXMOX_TOKEN_ID_3=... -# PROXMOX_TOKEN_SECRET_3=... -# ... +# --- Pulse Service Settings (Optional) --- +# Interval in milliseconds for fetching detailed VM/Container metrics (default: 2000) +# PULSE_METRIC_INTERVAL_MS=2000 +# Interval in milliseconds for fetching structural data (nodes, VM/CT list, storage) (default: 30000) +# PULSE_DISCOVERY_INTERVAL_MS=30000 - -# --- Proxmox Backup Server (PBS) Endpoint 1 (Optional) --- -PBS_HOST=your-pbs-ip-or-hostname -PBS_PORT=8007 # Optional, defaults to 8007 if omitted - -# Authentication: Use *either* API Token *or* User/Password. -# API Token (Recommended): -# PBS_TOKEN_ID=your-pbs-api-token-id@pbs!your-token-name # e.g., pulse-reader@pbs!pulse-token -# PBS_TOKEN_SECRET=your-pbs-api-token-secret-uuid - -# User/Password (Less Recommended): -PBS_USER=root@pam # e.g., root@pam or backup-user@pbs -PBS_PASSWORD=your-password -PBS_REALM=pbs # Optional, defaults to 'pbs' if using User/Password auth - -# Optional: Set a friendly name for this endpoint in logs/UI (defaults to host) -PBS_NODE_NAME=My Primary PBS - -# Optional: Allow self-signed certificates (defaults to true if omitted) -PBS_ALLOW_SELF_SIGNED_CERTS=true - - -# --- Proxmox Backup Server (PBS) Endpoint 2 (Optional) --- -# Duplicate these variables with a _2 suffix for a second PBS endpoint. -# PBS_HOST_2=second-pbs-ip-or-hostname -# PBS_PORT_2=8007 -# PBS_TOKEN_ID_2=token-id@pbs!token-name_2 -# PBS_TOKEN_SECRET_2=token-secret-uuid_2 -# PBS_NODE_NAME_2=My Second PBS -# PBS_ALLOW_SELF_SIGNED_CERTS_2=true -# ... User/Password_2 if needed ... - - -# --- Proxmox Backup Server (PBS) Endpoint 3, 4, ... (Optional) --- -# Continue the pattern with _3, _4, etc. for more endpoints. -# PBS_HOST_3=... -# PBS_TOKEN_ID_3=... -# PBS_TOKEN_SECRET_3=... -# ... - - -# Development Settings -ENABLE_HOT_RELOAD=true # Set to false to disable hot reload - -# --- Pulse Interval Configuration (Optional) --- -# Adjust how frequently Pulse fetches data. Lower values mean more real-time data -# but increase load on PVE/PBS hosts. Values are in milliseconds. -# -# PULSE_METRIC_INTERVAL_MS=2000 # Default: 2000 (2 seconds) - Frequency for fetching guest CPU/Mem/IO stats. -# PULSE_DISCOVERY_INTERVAL_MS=30000 # Default: 30000 (30 seconds) - Frequency for fetching node/guest lists and PBS data. \ No newline at end of file +# --- Development Settings (Optional) --- +# Enable/disable hot reloading for frontend changes (default: true) +# ENABLE_HOT_RELOAD=true \ No newline at end of file diff --git a/server/index.js b/server/index.js index eb653930a..c2d95b40b 100644 --- a/server/index.js +++ b/server/index.js @@ -1,11 +1,13 @@ require('dotenv').config(); // Load environment variables from .env file // --- BEGIN Environment Variable Validation --- +// Define required primary variables (API Token Auth Only) const primaryRequiredEnvVars = [ 'PROXMOX_HOST', 'PROXMOX_TOKEN_ID', 'PROXMOX_TOKEN_SECRET' ]; + const placeholderValues = [ 'your-proxmox-ip-or-hostname', 'your-api-token-id@pam!your-token-name', @@ -52,11 +54,6 @@ endpoints.push({ tokenSecret: process.env.PROXMOX_TOKEN_SECRET, enabled: process.env.PROXMOX_ENABLED !== 'false', // Currently unused, but kept for potential future use allowSelfSignedCerts: process.env.PROXMOX_ALLOW_SELF_SIGNED_CERTS !== 'false', - credentials: { // Keep password fallback possibility - username: process.env.PROXMOX_USERNAME, - password: process.env.PROXMOX_PASSWORD, - realm: process.env.PROXMOX_REALM || 'pam' - } }); // Load additional endpoints (PROXMOX_HOST_2, _3, ...) @@ -87,11 +84,6 @@ while (process.env[`PROXMOX_HOST_${i}`]) { tokenSecret: tokenSecret, enabled: process.env[`PROXMOX_ENABLED_${i}`] !== 'false', allowSelfSignedCerts: process.env[`PROXMOX_ALLOW_SELF_SIGNED_CERTS_${i}`] !== 'false', - credentials: { // Allow fallback for additional endpoints too - username: process.env[`PROXMOX_USERNAME_${i}`], - password: process.env[`PROXMOX_PASSWORD_${i}`], - realm: process.env[`PROXMOX_REALM_${i}`] || 'pam' - } }); i++; } @@ -132,9 +124,6 @@ const pbsApiClients = {}; // Object to hold initialized clients, keyed by pbsCon function loadPbsConfig(index = null) { const suffix = index ? `_${index}` : ''; const hostVar = `PBS_HOST${suffix}`; - const userVar = `PBS_USER${suffix}`; - const passVar = `PBS_PASSWORD${suffix}`; - const realmVar = `PBS_REALM${suffix}`; const tokenIdVar = `PBS_TOKEN_ID${suffix}`; const tokenSecretVar = `PBS_TOKEN_SECRET${suffix}`; const nodeNameVar = `PBS_NODE_NAME${suffix}`; @@ -157,40 +146,14 @@ function loadPbsConfig(index = null) { } // ---> END ADDED <---\ - const pbsUser = process.env[userVar]; - const pbsPassword = process.env[passVar]; const pbsTokenId = process.env[tokenIdVar]; const pbsTokenSecret = process.env[tokenSecretVar]; let config = null; let idPrefix = index ? `pbs_endpoint_${index}` : 'pbs_primary'; - // Check User/Password first - if (pbsUser && pbsPassword) { - const pbsPlaceholders = placeholderValues.filter(p => - pbsHostUrl.includes(p) || pbsUser.includes(p) || pbsPassword.includes(p) // Check against URL - ); - if (pbsPlaceholders.length > 0) { - console.warn(`WARN: Skipping PBS configuration ${index || 'primary'} (User/Pass). Placeholder values detected for: ${pbsPlaceholders.join(', ')}`); - } else { - config = { - id: `${idPrefix}_userpass`, - authMethod: 'userpass', - name: process.env[nodeNameVar] || pbsHostname, // User-defined name or parsed hostname - host: pbsHostUrl, // Keep original full URL here - port: process.env[portVar] || '8007', - user: pbsUser, - password: pbsPassword, - realm: process.env[realmVar] || 'pbs', - nodeName: process.env[nodeNameVar], // Store explicitly set node name - allowSelfSignedCerts: process.env[selfSignedVar] !== 'false', - enabled: true // Assuming enabled if configured - }; - console.log(`INFO: Found PBS configuration ${index || 'primary'} (User/Password): ${config.name} (${config.host})`); - } - } - // Check Token second - else if (pbsTokenId && pbsTokenSecret) { + // Check Token ONLY + if (pbsTokenId && pbsTokenSecret) { const pbsPlaceholders = placeholderValues.filter(p => pbsHostUrl.includes(p) || pbsTokenId.includes(p) || pbsTokenSecret.includes(p) // Check against URL ); @@ -199,7 +162,7 @@ function loadPbsConfig(index = null) { } else { config = { id: `${idPrefix}_token`, - authMethod: 'token', + authMethod: 'token', // Explicitly set auth method name: process.env[nodeNameVar] || pbsHostname, host: pbsHostUrl, // Keep original full URL here port: process.env[portVar] || '8007', @@ -214,14 +177,16 @@ function loadPbsConfig(index = null) { } // Warn if host is set but auth is incomplete else { - console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set either (${userVar} + ${passVar}) or (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`); + console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`); } if (config) { pbsConfigs.push(config); return true; // Indicate a config was found and added } - return true; // Indicate we should check the next index even if this one was partial/invalid + // If host was present but token details were missing/invalid, return true to check next index + // but don't push an incomplete config. + return !!pbsHostUrl; // Return true if host was set, false otherwise } // Load primary PBS config (index=null) @@ -262,18 +227,14 @@ endpoints.forEach(endpoint => { // Add request interceptor for authentication (specific to this endpoint) apiClient.interceptors.request.use(config => { - // Add API token authentication + // Add API token authentication ONLY if (endpoint.tokenId && endpoint.tokenSecret) { config.headers.Authorization = `PVEAPIToken=${endpoint.tokenId}=${endpoint.tokenSecret}`; - } - // Fallback to password auth if configured FOR THIS ENDPOINT - else if (endpoint.credentials && endpoint.credentials.username && endpoint.credentials.password) { - const { username, password, realm } = endpoint.credentials; - config.headers.Authorization = `Basic ${Buffer.from(`${username}@${realm}:${password}`).toString('base64')}`; - if (!realm) console.warn(`WARN: Using password auth for endpoint ${endpoint.name} without a realm specified. Defaulting to 'pam'.`); } else { - // Should not happen due to validation, but log just in case - console.error(`ERROR: Endpoint ${endpoint.name} has neither token nor full username/password credentials configured.`); + // This should ideally not happen if validation passed, but log error. + console.error(`ERROR: Endpoint ${endpoint.name} is missing required API token credentials.`); + // Optionally, you could cancel the request here: + // return Promise.reject(new Error(`Missing API token for endpoint ${endpoint.name}`)); } return config; }); @@ -300,75 +261,6 @@ endpoints.forEach(endpoint => { }); // --- Create PBS API Client (if configured) --- -async function getPbsAuthTicketAndSetupClient(config) { - const pbsBaseURL = config.host.includes('://') - ? `${config.host}` // Assume full URL if :// is present - : `https://${config.host}:${config.port}`; - - const loginUrl = `${pbsBaseURL}/api2/json/access/ticket`; - const httpsAgent = new https.Agent({ - rejectUnauthorized: !config.allowSelfSignedCerts - }); - - try { - console.log(`INFO: Attempting PBS login for user ${config.user}...`); - const response = await axios.post(loginUrl, `username=${encodeURIComponent(config.user)}&password=${encodeURIComponent(config.password)}`, { - httpsAgent: httpsAgent, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }); - - const ticket = response.data.data.ticket; - const csrfToken = response.data.data.CSRFPreventionToken; - const username = response.data.data.username; - - if (!ticket || !csrfToken) { - throw new Error("Login response missing ticket or CSRF token."); - } - - console.log(`INFO: PBS login successful for ${username}. Setting up authenticated client.`); - - // Create the authenticated client instance - const pbsAuthenticatedClient = axios.create({ - baseURL: `${pbsBaseURL}/api2/json`, - httpsAgent: httpsAgent, - headers: { - 'Content-Type': 'application/json' - } - }); - - // Apply interceptor for cookie and CSRF token - pbsAuthenticatedClient.interceptors.request.use(reqConfig => { - reqConfig.headers['CSRFPreventionToken'] = csrfToken; - reqConfig.headers['Cookie'] = `PBSAuthCookie=${ticket}`; - return reqConfig; - }); - - // Apply retry logic - axiosRetry(pbsAuthenticatedClient, { - retries: 3, - retryDelay: (retryCount, error) => { - console.warn(`Retrying authenticated PBS API request (attempt ${retryCount}) due to error: ${error.message}`); - return axiosRetry.exponentialDelay(retryCount); - }, - retryCondition: (error) => { - // Also retry on 401/403 in case ticket expires or CSRF mismatch? - // For now, just standard retry conditions. - return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error); - }, - }); - - return { client: pbsAuthenticatedClient, config: config }; // Return the fully configured client object - - } catch (error) { - console.error(`ERROR: PBS login failed for user ${config.user}: ${error.message}`); - if (error.response) { - console.error(`ERROR: PBS login response status: ${error.response.status}`); - console.error(`ERROR: PBS login response data: ${JSON.stringify(error.response.data)}`); - } - return null; // Indicate failure - } -} - async function initializeAllPbsClients() { if (pbsConfigs.length === 0) return; @@ -376,14 +268,8 @@ async function initializeAllPbsClients() { const initPromises = pbsConfigs.map(async (config) => { let clientData = null; try { - if (config.authMethod === 'userpass') { - clientData = await getPbsAuthTicketAndSetupClient(config); - if (clientData) { - console.log(`INFO: [PBS Init] Successfully initialized client for instance '${config.name}' (User/Password Auth)`); - } else { - console.error(`ERROR: Failed to initialize PBS client (User/Password) for: ${config.name} (${config.host})`); - } - } else if (config.authMethod === 'token') { + // We only support token auth now + if (config.authMethod === 'token') { // Token Auth Logic (adapted from old initializePbsClient) const pbsBaseURL = config.host.includes('://') ? `${config.host}/api2/json` @@ -398,7 +284,7 @@ async function initializeAllPbsClients() { }); pbsAxiosInstance.interceptors.request.use(reqConfig => { - reqConfig.headers.Authorization = `PBSAPIToken ${config.tokenId}:${config.tokenSecret}`; + reqConfig.headers.Authorization = `PBSAPIToken ${config.tokenId}=${config.tokenSecret}`; return reqConfig; }); @@ -416,7 +302,8 @@ async function initializeAllPbsClients() { clientData = { client: pbsAxiosInstance, config: config }; console.log(`INFO: [PBS Init] Successfully initialized client for instance '${config.name}' (Token Auth)`); } else { - console.error(`ERROR: Unknown authMethod '${config.authMethod}' for PBS config: ${config.name}`); + // 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}`); } if (clientData) {