Add configurable backup polling interval
This commit is contained in:
parent
d15ad1d0b4
commit
0b4e4f9c59
12 changed files with 547 additions and 116 deletions
|
|
@ -314,6 +314,8 @@ These env vars override system.json values. When set, the UI will show a warning
|
|||
- `CONNECTION_TIMEOUT` - API timeout in seconds (default: 10)
|
||||
- `ALLOWED_ORIGINS` - CORS origins (default: same-origin only)
|
||||
- `LOG_LEVEL` - Log verbosity: debug/info/warn/error (default: info)
|
||||
- `ENABLE_BACKUP_POLLING` - Set to `false` to disable polling of Proxmox backup/snapshot APIs (default: true)
|
||||
- `BACKUP_POLLING_INTERVAL` - Override the backup polling cadence. Accepts Go duration syntax (e.g. `30m`, `6h`) or seconds. Use `0` for Pulse's default (~90s) cadence.
|
||||
- `PULSE_PUBLIC_URL` - Full URL to access Pulse (e.g., `http://192.168.1.100:7655`)
|
||||
- **Auto-detected** if not set (except inside Docker where detection is disabled)
|
||||
- Used in webhook notifications for "View in Pulse" links
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ export interface SystemSettings {
|
|||
autoUpdateEnabled: boolean;
|
||||
autoUpdateCheckInterval?: number;
|
||||
autoUpdateTime?: string;
|
||||
backupPollingInterval?: number;
|
||||
backupPollingEnabled?: boolean;
|
||||
// apiToken removed - now handled via security API
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -257,6 +257,18 @@ const SETTINGS_HEADER_META: Record<SettingsTab, { title: string; description: st
|
|||
},
|
||||
};
|
||||
|
||||
const BACKUP_INTERVAL_OPTIONS = [
|
||||
{ label: 'Default (~90 seconds)', value: 0 },
|
||||
{ label: '15 minutes', value: 15 * 60 },
|
||||
{ label: '30 minutes', value: 30 * 60 },
|
||||
{ label: '1 hour', value: 60 * 60 },
|
||||
{ label: '6 hours', value: 6 * 60 * 60 },
|
||||
{ label: '12 hours', value: 12 * 60 * 60 },
|
||||
{ label: '24 hours', value: 24 * 60 * 60 },
|
||||
];
|
||||
|
||||
const BACKUP_INTERVAL_MAX_MINUTES = 7 * 24 * 60; // 7 days
|
||||
|
||||
// Node with UI-specific fields
|
||||
type NodeConfigWithStatus = NodeConfig & {
|
||||
hasPassword?: boolean;
|
||||
|
|
@ -363,6 +375,36 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
const [autoUpdateEnabled, setAutoUpdateEnabled] = createSignal(false);
|
||||
const [autoUpdateCheckInterval, setAutoUpdateCheckInterval] = createSignal(24);
|
||||
const [autoUpdateTime, setAutoUpdateTime] = createSignal('03:00');
|
||||
const [backupPollingEnabled, setBackupPollingEnabled] = createSignal(true);
|
||||
const [backupPollingInterval, setBackupPollingInterval] = createSignal(0);
|
||||
const [backupPollingCustomMinutes, setBackupPollingCustomMinutes] = createSignal(60);
|
||||
const backupPollingEnvLocked = () =>
|
||||
Boolean(envOverrides()['ENABLE_BACKUP_POLLING'] || envOverrides()['BACKUP_POLLING_INTERVAL']);
|
||||
const backupIntervalSelectValue = () => {
|
||||
const seconds = backupPollingInterval();
|
||||
return BACKUP_INTERVAL_OPTIONS.some((option) => option.value === seconds)
|
||||
? String(seconds)
|
||||
: 'custom';
|
||||
};
|
||||
const backupIntervalSummary = () => {
|
||||
if (!backupPollingEnabled()) {
|
||||
return 'Backup polling is disabled.';
|
||||
}
|
||||
const seconds = backupPollingInterval();
|
||||
if (seconds <= 0) {
|
||||
return 'Pulse checks backups and snapshots at the default cadence (~every 90 seconds).';
|
||||
}
|
||||
if (seconds % 86400 === 0) {
|
||||
const days = seconds / 86400;
|
||||
return `Pulse checks backups every ${days === 1 ? 'day' : `${days} days`}.`;
|
||||
}
|
||||
if (seconds % 3600 === 0) {
|
||||
const hours = seconds / 3600;
|
||||
return `Pulse checks backups every ${hours === 1 ? 'hour' : `${hours} hours`}.`;
|
||||
}
|
||||
const minutes = Math.max(1, Math.round(seconds / 60));
|
||||
return `Pulse checks backups every ${minutes === 1 ? 'minute' : `${minutes} minutes`}.`;
|
||||
};
|
||||
|
||||
// Diagnostics
|
||||
const [diagnosticsData, setDiagnosticsData] = createSignal<DiagnosticsData | null>(null);
|
||||
|
|
@ -988,6 +1030,20 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
// Load embedding settings
|
||||
setAllowEmbedding(systemSettings.allowEmbedding ?? false);
|
||||
setAllowedEmbedOrigins(systemSettings.allowedEmbedOrigins || '');
|
||||
// Backup polling controls
|
||||
if (typeof systemSettings.backupPollingEnabled === 'boolean') {
|
||||
setBackupPollingEnabled(systemSettings.backupPollingEnabled);
|
||||
} else {
|
||||
setBackupPollingEnabled(true);
|
||||
}
|
||||
const intervalSeconds =
|
||||
typeof systemSettings.backupPollingInterval === 'number'
|
||||
? Math.max(0, Math.floor(systemSettings.backupPollingInterval))
|
||||
: 0;
|
||||
setBackupPollingInterval(intervalSeconds);
|
||||
if (intervalSeconds > 0) {
|
||||
setBackupPollingCustomMinutes(Math.max(1, Math.round(intervalSeconds / 60)));
|
||||
}
|
||||
// Load auto-update settings
|
||||
setAutoUpdateEnabled(systemSettings.autoUpdateEnabled || false);
|
||||
setAutoUpdateCheckInterval(systemSettings.autoUpdateCheckInterval || 24);
|
||||
|
|
@ -1077,6 +1133,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
autoUpdateEnabled: autoUpdateEnabled(),
|
||||
autoUpdateCheckInterval: autoUpdateCheckInterval(),
|
||||
autoUpdateTime: autoUpdateTime(),
|
||||
backupPollingEnabled: backupPollingEnabled(),
|
||||
backupPollingInterval: backupPollingInterval(),
|
||||
allowEmbedding: allowEmbedding(),
|
||||
allowedEmbedOrigins: allowedEmbedOrigins(),
|
||||
});
|
||||
|
|
@ -3418,12 +3476,160 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
border={false}
|
||||
class="border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<SectionHeader
|
||||
title="Backup & restore"
|
||||
description="Backup your node configurations and credentials or restore from a previous backup."
|
||||
size="md"
|
||||
class="mb-4"
|
||||
/>
|
||||
<section class="space-y-3">
|
||||
<h4 class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="9" stroke-width="2" />
|
||||
<path d="M12 7v5l3 3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Backup polling
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Control how often Pulse queries Proxmox backup tasks, datastore contents, and guest snapshots.
|
||||
Longer intervals reduce disk activity and API load.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Enable backup polling
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Required for dashboard backup status, storage snapshots, and alerting.
|
||||
</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
checked={backupPollingEnabled()}
|
||||
disabled={backupPollingEnvLocked()}
|
||||
onChange={(e) => {
|
||||
setBackupPollingEnabled(e.currentTarget.checked);
|
||||
if (!backupPollingEnvLocked()) {
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600 peer-disabled:opacity-50"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Show when={backupPollingEnabled()}>
|
||||
<div class="space-y-3 rounded-md border border-gray-200 dark:border-gray-600 p-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Polling interval
|
||||
</label>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{backupIntervalSummary()}
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
value={backupIntervalSelectValue()}
|
||||
disabled={backupPollingEnvLocked()}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (value === 'custom') {
|
||||
const minutes = Math.max(1, backupPollingCustomMinutes());
|
||||
setBackupPollingInterval(minutes * 60);
|
||||
} else {
|
||||
const seconds = parseInt(value, 10);
|
||||
if (!Number.isNaN(seconds)) {
|
||||
setBackupPollingInterval(seconds);
|
||||
if (seconds > 0) {
|
||||
setBackupPollingCustomMinutes(Math.max(1, Math.round(seconds / 60)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!backupPollingEnvLocked()) {
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
}}
|
||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 disabled:opacity-50"
|
||||
>
|
||||
<For each={BACKUP_INTERVAL_OPTIONS}>
|
||||
{(option) => (
|
||||
<option value={String(option.value)}>{option.label}</option>
|
||||
)}
|
||||
</For>
|
||||
<option value="custom">Custom interval…</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Show when={backupIntervalSelectValue() === 'custom'}>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
Custom interval (minutes)
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={BACKUP_INTERVAL_MAX_MINUTES}
|
||||
value={backupPollingCustomMinutes()}
|
||||
disabled={backupPollingEnvLocked()}
|
||||
onInput={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
if (Number.isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
const clamped = Math.max(
|
||||
1,
|
||||
Math.min(BACKUP_INTERVAL_MAX_MINUTES, Math.floor(value)),
|
||||
);
|
||||
setBackupPollingCustomMinutes(clamped);
|
||||
setBackupPollingInterval(clamped * 60);
|
||||
if (!backupPollingEnvLocked()) {
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
}}
|
||||
class="w-24 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 disabled:opacity-50"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
1 – {BACKUP_INTERVAL_MAX_MINUTES} minutes (≈7 days max)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={backupPollingEnvLocked()}>
|
||||
<div class="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20 p-3 text-xs text-amber-700 dark:text-amber-200">
|
||||
<svg
|
||||
class="w-4 h-4 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-medium">Environment override detected</p>
|
||||
<p class="mt-1">
|
||||
The <code class="font-mono">ENABLE_BACKUP_POLLING</code> or{' '}
|
||||
<code class="font-mono">BACKUP_POLLING_INTERVAL</code> environment
|
||||
variables are set. Remove them and restart Pulse to manage backup polling here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SectionHeader
|
||||
title="Backup & restore"
|
||||
description="Backup your node configurations and credentials or restore from a previous backup."
|
||||
size="md"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Export Section */}
|
||||
|
|
|
|||
|
|
@ -621,11 +621,17 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
: 'text-green-600 dark:text-green-400';
|
||||
|
||||
const temp = node!.temperature;
|
||||
const hasMinMax = temp && temp.cpuMin && temp.cpuMin > 0 && temp.cpuMaxRecord && temp.cpuMaxRecord > 0;
|
||||
const cpuMin = temp?.cpuMin;
|
||||
const cpuMax = temp?.cpuMaxRecord;
|
||||
const hasMinMax =
|
||||
typeof cpuMin === 'number' &&
|
||||
cpuMin > 0 &&
|
||||
typeof cpuMax === 'number' &&
|
||||
cpuMax > 0;
|
||||
|
||||
if (hasMinMax) {
|
||||
const min = Math.round(temp.cpuMin);
|
||||
const max = Math.round(temp.cpuMaxRecord);
|
||||
const min = Math.round(cpuMin);
|
||||
const max = Math.round(cpuMax);
|
||||
|
||||
const getTooltipColor = (temp: number) => {
|
||||
if (temp >= 80) return 'text-red-400';
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export interface SystemConfig {
|
|||
updateChannel?: string; // Update channel: 'stable' | 'rc' | 'beta'
|
||||
autoUpdateCheckInterval?: number; // Hours between update checks
|
||||
autoUpdateTime?: string; // Time for updates (HH:MM format)
|
||||
backupPollingInterval?: number; // Backup polling interval in seconds (0 = default cadence)
|
||||
backupPollingEnabled?: boolean; // Enable backup polling of PVE/PBS data
|
||||
allowedOrigins?: string; // CORS allowed origins
|
||||
backendPort?: number; // Backend API port (default: 7655)
|
||||
frontendPort?: number; // Frontend UI port (default: 7655)
|
||||
|
|
@ -147,6 +149,8 @@ export const DEFAULT_CONFIG: {
|
|||
updateChannel: 'stable',
|
||||
autoUpdateCheckInterval: 24,
|
||||
autoUpdateTime: '03:00',
|
||||
backupPollingEnabled: true,
|
||||
backupPollingInterval: 0,
|
||||
allowedOrigins: '',
|
||||
backendPort: 7655,
|
||||
frontendPort: 7655,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export interface MonitoringSettings {
|
|||
// Note: PVE polling is hardcoded to 10s server-side
|
||||
concurrentPolling: boolean;
|
||||
backupPollingCycles: number;
|
||||
backupPollingIntervalMs: number;
|
||||
backupPollingEnabled: boolean;
|
||||
metricsRetentionDays: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2523,6 +2523,7 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
|||
settings := config.SystemSettings{
|
||||
// Note: PVE polling is hardcoded to 10s
|
||||
PBSPollingInterval: int(h.config.PBSPollingInterval.Seconds()),
|
||||
BackupPollingInterval: int(h.config.BackupPollingInterval.Seconds()),
|
||||
BackendPort: h.config.BackendPort,
|
||||
FrontendPort: h.config.FrontendPort,
|
||||
AllowedOrigins: h.config.AllowedOrigins,
|
||||
|
|
@ -2538,6 +2539,8 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
|||
DiscoveryEnabled: persistedSettings.DiscoveryEnabled, // Include discoveryEnabled from persisted settings
|
||||
DiscoverySubnet: persistedSettings.DiscoverySubnet, // Include discoverySubnet from persisted settings
|
||||
}
|
||||
backupEnabled := h.config.EnableBackupPolling
|
||||
settings.BackupPollingEnabled = &backupEnabled
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(settings)
|
||||
|
|
@ -2670,6 +2673,10 @@ func (h *ConfigHandlers) HandleUpdateSystemSettingsOLD(w http.ResponseWriter, r
|
|||
http.Error(w, "PBS polling interval must be positive", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if settings.BackupPollingInterval < 0 {
|
||||
http.Error(w, "Backup polling interval cannot be negative", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Update polling intervals
|
||||
needsReload := false
|
||||
|
|
@ -2679,6 +2686,14 @@ func (h *ConfigHandlers) HandleUpdateSystemSettingsOLD(w http.ResponseWriter, r
|
|||
h.config.PBSPollingInterval = time.Duration(settings.PBSPollingInterval) * time.Second
|
||||
needsReload = true
|
||||
}
|
||||
if settings.BackupPollingInterval > 0 || (settings.BackupPollingInterval == 0 && h.config.BackupPollingInterval != 0) {
|
||||
h.config.BackupPollingInterval = time.Duration(settings.BackupPollingInterval) * time.Second
|
||||
needsReload = true
|
||||
}
|
||||
if settings.BackupPollingEnabled != nil {
|
||||
h.config.EnableBackupPolling = *settings.BackupPollingEnabled
|
||||
needsReload = true
|
||||
}
|
||||
|
||||
// Trigger a monitor reload if intervals changed
|
||||
if needsReload && h.reloadFunc != nil {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,22 @@ func validateSystemSettings(settings *config.SystemSettings, rawRequest map[stri
|
|||
}
|
||||
}
|
||||
|
||||
if val, ok := rawRequest["backupPollingInterval"]; ok {
|
||||
if interval, ok := val.(float64); ok {
|
||||
if interval < 0 {
|
||||
return fmt.Errorf("Backup polling interval cannot be negative")
|
||||
}
|
||||
if interval > 0 && interval < 10 {
|
||||
return fmt.Errorf("Backup polling interval must be at least 10 seconds")
|
||||
}
|
||||
if interval > 604800 {
|
||||
return fmt.Errorf("Backup polling interval cannot exceed 604800 seconds (7 days)")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Backup polling interval must be a number")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate boolean fields have correct type
|
||||
if val, ok := rawRequest["autoUpdateEnabled"]; ok {
|
||||
if _, ok := val.(bool); !ok {
|
||||
|
|
@ -107,6 +123,12 @@ func validateSystemSettings(settings *config.SystemSettings, rawRequest map[stri
|
|||
}
|
||||
}
|
||||
|
||||
if val, ok := rawRequest["backupPollingEnabled"]; ok {
|
||||
if _, ok := val.(bool); !ok {
|
||||
return fmt.Errorf("backupPollingEnabled must be a boolean")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate auto-update check interval (min 1 hour, max 7 days)
|
||||
if val, ok := rawRequest["autoUpdateCheckInterval"]; ok {
|
||||
if interval, ok := val.(float64); ok {
|
||||
|
|
@ -184,6 +206,11 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
|||
log.Debug().
|
||||
Str("theme", settings.Theme).
|
||||
Msg("Loaded system settings for API response")
|
||||
|
||||
// Always expose effective backup polling configuration
|
||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||
enabled := h.config.EnableBackupPolling
|
||||
settings.BackupPollingEnabled = &enabled
|
||||
}
|
||||
|
||||
// Include env override information
|
||||
|
|
@ -279,6 +306,9 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
if _, ok := rawRequest["pmgPollingInterval"]; ok {
|
||||
settings.PMGPollingInterval = updates.PMGPollingInterval
|
||||
}
|
||||
if _, ok := rawRequest["backupPollingInterval"]; ok {
|
||||
settings.BackupPollingInterval = updates.BackupPollingInterval
|
||||
}
|
||||
if updates.AllowedOrigins != "" {
|
||||
settings.AllowedOrigins = updates.AllowedOrigins
|
||||
}
|
||||
|
|
@ -315,6 +345,9 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
if _, ok := rawRequest["allowEmbedding"]; ok {
|
||||
settings.AllowEmbedding = updates.AllowEmbedding
|
||||
}
|
||||
if _, ok := rawRequest["backupPollingEnabled"]; ok {
|
||||
settings.BackupPollingEnabled = updates.BackupPollingEnabled
|
||||
}
|
||||
|
||||
// Update the config
|
||||
// Note: PVE polling is hardcoded to 10s
|
||||
|
|
@ -327,6 +360,16 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
if settings.PMGPollingInterval > 0 {
|
||||
h.config.PMGPollingInterval = time.Duration(settings.PMGPollingInterval) * time.Second
|
||||
}
|
||||
if _, ok := rawRequest["backupPollingInterval"]; ok {
|
||||
if settings.BackupPollingInterval <= 0 {
|
||||
h.config.BackupPollingInterval = 0
|
||||
} else {
|
||||
h.config.BackupPollingInterval = time.Duration(settings.BackupPollingInterval) * time.Second
|
||||
}
|
||||
}
|
||||
if settings.BackupPollingEnabled != nil {
|
||||
h.config.EnableBackupPolling = *settings.BackupPollingEnabled
|
||||
}
|
||||
if settings.UpdateChannel != "" {
|
||||
h.config.UpdateChannel = settings.UpdateChannel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,13 +74,15 @@ type Config struct {
|
|||
|
||||
// Monitoring settings
|
||||
// Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s
|
||||
PBSPollingInterval time.Duration `envconfig:"PBS_POLLING_INTERVAL"` // PBS polling interval (60s default)
|
||||
PMGPollingInterval time.Duration `envconfig:"PMG_POLLING_INTERVAL"` // PMG polling interval (60s default)
|
||||
ConcurrentPolling bool `envconfig:"CONCURRENT_POLLING" default:"true"`
|
||||
ConnectionTimeout time.Duration `envconfig:"CONNECTION_TIMEOUT" default:"45s"` // Increased for slow storage operations
|
||||
MetricsRetentionDays int `envconfig:"METRICS_RETENTION_DAYS" default:"7"`
|
||||
BackupPollingCycles int `envconfig:"BACKUP_POLLING_CYCLES" default:"10"`
|
||||
WebhookBatchDelay time.Duration `envconfig:"WEBHOOK_BATCH_DELAY" default:"10s"`
|
||||
PBSPollingInterval time.Duration `envconfig:"PBS_POLLING_INTERVAL"` // PBS polling interval (60s default)
|
||||
PMGPollingInterval time.Duration `envconfig:"PMG_POLLING_INTERVAL"` // PMG polling interval (60s default)
|
||||
ConcurrentPolling bool `envconfig:"CONCURRENT_POLLING" default:"true"`
|
||||
ConnectionTimeout time.Duration `envconfig:"CONNECTION_TIMEOUT" default:"45s"` // Increased for slow storage operations
|
||||
MetricsRetentionDays int `envconfig:"METRICS_RETENTION_DAYS" default:"7"`
|
||||
BackupPollingCycles int `envconfig:"BACKUP_POLLING_CYCLES" default:"10"`
|
||||
BackupPollingInterval time.Duration `envconfig:"BACKUP_POLLING_INTERVAL"`
|
||||
EnableBackupPolling bool `envconfig:"ENABLE_BACKUP_POLLING" default:"true"`
|
||||
WebhookBatchDelay time.Duration `envconfig:"WEBHOOK_BATCH_DELAY" default:"10s"`
|
||||
|
||||
// Logging settings
|
||||
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
|
||||
|
|
@ -247,29 +249,31 @@ func Load() (*Config, error) {
|
|||
|
||||
// Initialize config with defaults
|
||||
cfg := &Config{
|
||||
BackendHost: "0.0.0.0",
|
||||
BackendPort: 3000,
|
||||
FrontendHost: "0.0.0.0",
|
||||
FrontendPort: 7655,
|
||||
ConfigPath: dataDir,
|
||||
DataPath: dataDir,
|
||||
ConcurrentPolling: true,
|
||||
ConnectionTimeout: 60 * time.Second,
|
||||
MetricsRetentionDays: 7,
|
||||
BackupPollingCycles: 10,
|
||||
WebhookBatchDelay: 10 * time.Second,
|
||||
LogLevel: "info",
|
||||
LogMaxSize: 100,
|
||||
LogMaxAge: 30,
|
||||
LogCompress: true,
|
||||
AllowedOrigins: "", // Empty means no CORS headers (same-origin only)
|
||||
IframeEmbeddingAllow: "SAMEORIGIN",
|
||||
PBSPollingInterval: 60 * time.Second, // Default PBS polling (slower)
|
||||
PMGPollingInterval: 60 * time.Second, // Default PMG polling (aggregated stats)
|
||||
DiscoveryEnabled: true,
|
||||
DiscoverySubnet: "auto",
|
||||
EnvOverrides: make(map[string]bool),
|
||||
OIDC: NewOIDCConfig(),
|
||||
BackendHost: "0.0.0.0",
|
||||
BackendPort: 3000,
|
||||
FrontendHost: "0.0.0.0",
|
||||
FrontendPort: 7655,
|
||||
ConfigPath: dataDir,
|
||||
DataPath: dataDir,
|
||||
ConcurrentPolling: true,
|
||||
ConnectionTimeout: 60 * time.Second,
|
||||
MetricsRetentionDays: 7,
|
||||
BackupPollingCycles: 10,
|
||||
BackupPollingInterval: 0,
|
||||
EnableBackupPolling: true,
|
||||
WebhookBatchDelay: 10 * time.Second,
|
||||
LogLevel: "info",
|
||||
LogMaxSize: 100,
|
||||
LogMaxAge: 30,
|
||||
LogCompress: true,
|
||||
AllowedOrigins: "", // Empty means no CORS headers (same-origin only)
|
||||
IframeEmbeddingAllow: "SAMEORIGIN",
|
||||
PBSPollingInterval: 60 * time.Second, // Default PBS polling (slower)
|
||||
PMGPollingInterval: 60 * time.Second, // Default PMG polling (aggregated stats)
|
||||
DiscoveryEnabled: true,
|
||||
DiscoverySubnet: "auto",
|
||||
EnvOverrides: make(map[string]bool),
|
||||
OIDC: NewOIDCConfig(),
|
||||
}
|
||||
|
||||
// Initialize persistence
|
||||
|
|
@ -301,6 +305,15 @@ func Load() (*Config, error) {
|
|||
cfg.PMGPollingInterval = time.Duration(systemSettings.PMGPollingInterval) * time.Second
|
||||
}
|
||||
|
||||
if systemSettings.BackupPollingInterval > 0 {
|
||||
cfg.BackupPollingInterval = time.Duration(systemSettings.BackupPollingInterval) * time.Second
|
||||
} else if systemSettings.BackupPollingInterval == 0 {
|
||||
cfg.BackupPollingInterval = 0
|
||||
}
|
||||
if systemSettings.BackupPollingEnabled != nil {
|
||||
cfg.EnableBackupPolling = *systemSettings.BackupPollingEnabled
|
||||
}
|
||||
|
||||
if systemSettings.UpdateChannel != "" {
|
||||
cfg.UpdateChannel = systemSettings.UpdateChannel
|
||||
}
|
||||
|
|
@ -370,6 +383,53 @@ func Load() (*Config, error) {
|
|||
// Limited environment variable support
|
||||
// NOTE: Node configuration is NOT done via env vars - use the web UI instead
|
||||
|
||||
if cyclesStr := strings.TrimSpace(os.Getenv("BACKUP_POLLING_CYCLES")); cyclesStr != "" {
|
||||
if cycles, err := strconv.Atoi(cyclesStr); err == nil {
|
||||
if cycles < 0 {
|
||||
log.Warn().Str("value", cyclesStr).Msg("Ignoring negative BACKUP_POLLING_CYCLES from environment")
|
||||
} else {
|
||||
cfg.BackupPollingCycles = cycles
|
||||
cfg.EnvOverrides["BACKUP_POLLING_CYCLES"] = true
|
||||
log.Info().Int("cycles", cycles).Msg("Overriding backup polling cycles from environment")
|
||||
}
|
||||
} else {
|
||||
log.Warn().Str("value", cyclesStr).Msg("Invalid BACKUP_POLLING_CYCLES value, ignoring")
|
||||
}
|
||||
}
|
||||
|
||||
if intervalStr := strings.TrimSpace(os.Getenv("BACKUP_POLLING_INTERVAL")); intervalStr != "" {
|
||||
if dur, err := time.ParseDuration(intervalStr); err == nil {
|
||||
if dur < 0 {
|
||||
log.Warn().Str("value", intervalStr).Msg("Ignoring negative BACKUP_POLLING_INTERVAL from environment")
|
||||
} else {
|
||||
cfg.BackupPollingInterval = dur
|
||||
cfg.EnvOverrides["BACKUP_POLLING_INTERVAL"] = true
|
||||
log.Info().Dur("interval", dur).Msg("Overriding backup polling interval from environment")
|
||||
}
|
||||
} else if seconds, err := strconv.Atoi(intervalStr); err == nil {
|
||||
if seconds < 0 {
|
||||
log.Warn().Str("value", intervalStr).Msg("Ignoring negative BACKUP_POLLING_INTERVAL (seconds) from environment")
|
||||
} else {
|
||||
cfg.BackupPollingInterval = time.Duration(seconds) * time.Second
|
||||
cfg.EnvOverrides["BACKUP_POLLING_INTERVAL"] = true
|
||||
log.Info().Int("seconds", seconds).Msg("Overriding backup polling interval (seconds) from environment")
|
||||
}
|
||||
} else {
|
||||
log.Warn().Str("value", intervalStr).Msg("Invalid BACKUP_POLLING_INTERVAL value, expected duration or seconds")
|
||||
}
|
||||
}
|
||||
|
||||
if enabledStr := strings.TrimSpace(os.Getenv("ENABLE_BACKUP_POLLING")); enabledStr != "" {
|
||||
switch strings.ToLower(enabledStr) {
|
||||
case "0", "false", "no", "off":
|
||||
cfg.EnableBackupPolling = false
|
||||
default:
|
||||
cfg.EnableBackupPolling = true
|
||||
}
|
||||
cfg.EnvOverrides["ENABLE_BACKUP_POLLING"] = true
|
||||
log.Info().Bool("enabled", cfg.EnableBackupPolling).Msg("Overriding backup polling enabled flag from environment")
|
||||
}
|
||||
|
||||
// Support both FRONTEND_PORT (preferred) and PORT (legacy) env vars
|
||||
if frontendPort := os.Getenv("FRONTEND_PORT"); frontendPort != "" {
|
||||
if p, err := strconv.Atoi(frontendPort); err == nil {
|
||||
|
|
|
|||
|
|
@ -541,6 +541,8 @@ type SystemSettings struct {
|
|||
// Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s
|
||||
PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds
|
||||
PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds
|
||||
BackupPollingInterval int `json:"backupPollingInterval,omitempty"`
|
||||
BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"`
|
||||
BackendPort int `json:"backendPort,omitempty"`
|
||||
FrontendPort int `json:"frontendPort,omitempty"`
|
||||
AllowedOrigins string `json:"allowedOrigins,omitempty"`
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@ type PortSettings struct {
|
|||
|
||||
// MonitoringSettings contains monitoring-related configuration
|
||||
type MonitoringSettings struct {
|
||||
PollingInterval int `json:"pollingInterval" yaml:"pollingInterval" mapstructure:"pollingInterval"` // milliseconds
|
||||
ConcurrentPolling bool `json:"concurrentPolling" yaml:"concurrentPolling" mapstructure:"concurrentPolling"`
|
||||
BackupPollingCycles int `json:"backupPollingCycles" yaml:"backupPollingCycles" mapstructure:"backupPollingCycles"` // How often to poll backups
|
||||
MetricsRetentionDays int `json:"metricsRetentionDays" yaml:"metricsRetentionDays" mapstructure:"metricsRetentionDays"`
|
||||
PollingInterval int `json:"pollingInterval" yaml:"pollingInterval" mapstructure:"pollingInterval"` // milliseconds
|
||||
ConcurrentPolling bool `json:"concurrentPolling" yaml:"concurrentPolling" mapstructure:"concurrentPolling"`
|
||||
BackupPollingCycles int `json:"backupPollingCycles" yaml:"backupPollingCycles" mapstructure:"backupPollingCycles"` // How often to poll backups
|
||||
BackupPollingIntervalMs int `json:"backupPollingIntervalMs" yaml:"backupPollingIntervalMs" mapstructure:"backupPollingIntervalMs"` // 0 = use cycle-based scheduling
|
||||
BackupPollingEnabled bool `json:"backupPollingEnabled" yaml:"backupPollingEnabled" mapstructure:"backupPollingEnabled"`
|
||||
MetricsRetentionDays int `json:"metricsRetentionDays" yaml:"metricsRetentionDays" mapstructure:"metricsRetentionDays"`
|
||||
}
|
||||
|
||||
// LoggingSettings contains logging configuration
|
||||
|
|
@ -66,10 +68,12 @@ func DefaultSettings() *Settings {
|
|||
},
|
||||
},
|
||||
Monitoring: MonitoringSettings{
|
||||
PollingInterval: 5000, // 5 seconds
|
||||
ConcurrentPolling: true,
|
||||
BackupPollingCycles: 10, // Poll backups every 10 cycles
|
||||
MetricsRetentionDays: 7,
|
||||
PollingInterval: 5000, // 5 seconds
|
||||
ConcurrentPolling: true,
|
||||
BackupPollingCycles: 10, // Poll backups every 10 cycles
|
||||
BackupPollingIntervalMs: 0,
|
||||
BackupPollingEnabled: true,
|
||||
MetricsRetentionDays: 7,
|
||||
},
|
||||
Logging: LoggingSettings{
|
||||
Level: "info",
|
||||
|
|
@ -118,8 +122,12 @@ func (s *Settings) Validate() error {
|
|||
return fmt.Errorf("polling interval must be at least 1000ms (1 second)")
|
||||
}
|
||||
|
||||
if s.Monitoring.BackupPollingCycles < 1 {
|
||||
return fmt.Errorf("backup polling cycles must be at least 1")
|
||||
if s.Monitoring.BackupPollingCycles < 0 {
|
||||
return fmt.Errorf("backup polling cycles cannot be negative")
|
||||
}
|
||||
|
||||
if s.Monitoring.BackupPollingIntervalMs < 0 {
|
||||
return fmt.Errorf("backup polling interval cannot be negative")
|
||||
}
|
||||
|
||||
// Validate logging level
|
||||
|
|
|
|||
|
|
@ -269,6 +269,8 @@ type Monitor struct {
|
|||
lastAuthAttempt map[string]time.Time // Track last auth attempt time
|
||||
lastClusterCheck map[string]time.Time // Track last cluster check for standalone nodes
|
||||
lastPhysicalDiskPoll map[string]time.Time // Track last physical disk poll time per instance
|
||||
lastPVEBackupPoll map[string]time.Time // Track last PVE backup poll per instance
|
||||
lastPBSBackupPoll map[string]time.Time // Track last PBS backup poll per instance
|
||||
persistence *config.ConfigPersistence // Add persistence for saving updated configs
|
||||
pbsBackupPollers map[string]bool // Track PBS backup polling goroutines per instance
|
||||
runtimeCtx context.Context // Context used while monitor is running
|
||||
|
|
@ -318,6 +320,42 @@ func safeFloat(val float64) float64 {
|
|||
return val
|
||||
}
|
||||
|
||||
// shouldRunBackupPoll determines whether a backup polling cycle should execute.
|
||||
// Returns whether polling should run, a human-readable skip reason, and the timestamp to record.
|
||||
func (m *Monitor) shouldRunBackupPoll(last time.Time, now time.Time) (bool, string, time.Time) {
|
||||
if m == nil || m.config == nil {
|
||||
return false, "configuration unavailable", last
|
||||
}
|
||||
|
||||
if !m.config.EnableBackupPolling {
|
||||
return false, "backup polling globally disabled", last
|
||||
}
|
||||
|
||||
interval := m.config.BackupPollingInterval
|
||||
if interval > 0 {
|
||||
if !last.IsZero() && now.Sub(last) < interval {
|
||||
next := last.Add(interval)
|
||||
return false, fmt.Sprintf("next run scheduled for %s", next.Format(time.RFC3339)), last
|
||||
}
|
||||
return true, "", now
|
||||
}
|
||||
|
||||
backupCycles := m.config.BackupPollingCycles
|
||||
if backupCycles <= 0 {
|
||||
backupCycles = 10
|
||||
}
|
||||
|
||||
if m.pollCounter%int64(backupCycles) == 0 || m.pollCounter == 1 {
|
||||
return true, "", now
|
||||
}
|
||||
|
||||
remaining := int64(backupCycles) - (m.pollCounter % int64(backupCycles))
|
||||
if remaining <= 0 {
|
||||
remaining = int64(backupCycles)
|
||||
}
|
||||
return false, fmt.Sprintf("next run in %d polling cycles", remaining), last
|
||||
}
|
||||
|
||||
const (
|
||||
dockerConnectionPrefix = "docker-"
|
||||
dockerOfflineGraceMultiplier = 4
|
||||
|
|
@ -1286,6 +1324,8 @@ func New(cfg *config.Config) (*Monitor, error) {
|
|||
lastAuthAttempt: make(map[string]time.Time),
|
||||
lastClusterCheck: make(map[string]time.Time),
|
||||
lastPhysicalDiskPoll: make(map[string]time.Time),
|
||||
lastPVEBackupPoll: make(map[string]time.Time),
|
||||
lastPBSBackupPoll: make(map[string]time.Time),
|
||||
persistence: config.NewConfigPersistence(cfg.DataPath),
|
||||
pbsBackupPollers: make(map[string]bool),
|
||||
nodeSnapshots: make(map[string]NodeMemorySnapshot),
|
||||
|
|
@ -3062,44 +3102,70 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
|||
}
|
||||
}
|
||||
|
||||
// Poll backups if enabled - using configurable cycle count
|
||||
// This prevents slow backup/snapshot queries from blocking real-time stats
|
||||
// Also poll on first cycle (pollCounter == 1) to ensure data loads quickly
|
||||
backupCycles := 10 // default
|
||||
if m.config.BackupPollingCycles > 0 {
|
||||
backupCycles = m.config.BackupPollingCycles
|
||||
}
|
||||
if instanceCfg.MonitorBackups && (m.pollCounter%int64(backupCycles) == 0 || m.pollCounter == 1) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
// Run backup polling in a separate goroutine to not block main polling
|
||||
go func() {
|
||||
timeout := m.calculateBackupOperationTimeout(instanceName)
|
||||
startTime := time.Now()
|
||||
log.Info().
|
||||
Str("instance", instanceName).
|
||||
Dur("timeout", timeout).
|
||||
Msg("Starting background backup/snapshot polling")
|
||||
// Create a separate context with longer timeout for backup operations
|
||||
backupCtx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
// Poll backups if enabled - respect configured interval or cycle gating
|
||||
if instanceCfg.MonitorBackups {
|
||||
if !m.config.EnableBackupPolling {
|
||||
log.Debug().
|
||||
Str("instance", instanceName).
|
||||
Msg("Skipping backup polling - globally disabled")
|
||||
} else {
|
||||
now := time.Now()
|
||||
|
||||
// Poll backup tasks
|
||||
m.pollBackupTasks(backupCtx, instanceName, client)
|
||||
m.mu.RLock()
|
||||
lastPoll := m.lastPVEBackupPoll[instanceName]
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Poll storage backups - pass nodes to avoid duplicate API calls
|
||||
m.pollStorageBackupsWithNodes(backupCtx, instanceName, client, nodes)
|
||||
shouldPoll, reason, newLast := m.shouldRunBackupPoll(lastPoll, now)
|
||||
if !shouldPoll {
|
||||
if reason != "" {
|
||||
log.Debug().
|
||||
Str("instance", instanceName).
|
||||
Str("reason", reason).
|
||||
Msg("Skipping PVE backup polling this cycle")
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
m.mu.Lock()
|
||||
m.lastPVEBackupPoll[instanceName] = newLast
|
||||
m.mu.Unlock()
|
||||
|
||||
// Poll guest snapshots
|
||||
m.pollGuestSnapshots(backupCtx, instanceName, client)
|
||||
// Run backup polling in a separate goroutine to avoid blocking real-time stats
|
||||
go func(startTime time.Time, inst string, pveClient PVEClientInterface) {
|
||||
timeout := m.calculateBackupOperationTimeout(inst)
|
||||
log.Info().
|
||||
Str("instance", inst).
|
||||
Dur("timeout", timeout).
|
||||
Msg("Starting background backup/snapshot polling")
|
||||
|
||||
log.Info().
|
||||
Str("instance", instanceName).
|
||||
Dur("duration", time.Since(startTime)).
|
||||
Msg("Completed background backup/snapshot polling")
|
||||
}()
|
||||
// Create a separate context with longer timeout for backup operations
|
||||
backupCtx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Poll backup tasks
|
||||
m.pollBackupTasks(backupCtx, inst, pveClient)
|
||||
|
||||
// Poll storage backups - pass nodes to avoid duplicate API calls
|
||||
m.pollStorageBackupsWithNodes(backupCtx, inst, pveClient, nodes)
|
||||
|
||||
// Poll guest snapshots
|
||||
m.pollGuestSnapshots(backupCtx, inst, pveClient)
|
||||
|
||||
duration := time.Since(startTime)
|
||||
log.Info().
|
||||
Str("instance", inst).
|
||||
Dur("duration", duration).
|
||||
Msg("Completed background backup/snapshot polling")
|
||||
|
||||
// Record actual completion time for interval scheduling
|
||||
m.mu.Lock()
|
||||
m.lastPVEBackupPoll[inst] = time.Now()
|
||||
m.mu.Unlock()
|
||||
}(now, instanceName, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4963,49 +5029,64 @@ func (m *Monitor) pollPBSInstance(ctx context.Context, instanceName string, clie
|
|||
Str("instance", instanceName).
|
||||
Msg("No PBS datastores available for backup polling")
|
||||
} else {
|
||||
backupCycles := 10
|
||||
if m.config.BackupPollingCycles > 0 {
|
||||
backupCycles = m.config.BackupPollingCycles
|
||||
}
|
||||
|
||||
shouldPoll := m.pollCounter%int64(backupCycles) == 0 || m.pollCounter == 1
|
||||
if !shouldPoll {
|
||||
if !m.config.EnableBackupPolling {
|
||||
log.Debug().
|
||||
Str("instance", instanceName).
|
||||
Int64("cycle", m.pollCounter).
|
||||
Int("backupCycles", backupCycles).
|
||||
Msg("Skipping PBS backup polling this cycle")
|
||||
Msg("Skipping PBS backup polling - globally disabled")
|
||||
} else {
|
||||
m.mu.Lock()
|
||||
if m.pbsBackupPollers[instanceName] {
|
||||
m.mu.Unlock()
|
||||
log.Debug().
|
||||
Str("instance", instanceName).
|
||||
Msg("PBS backup polling already in progress")
|
||||
} else {
|
||||
m.pbsBackupPollers[instanceName] = true
|
||||
m.mu.Unlock()
|
||||
now := time.Now()
|
||||
|
||||
m.mu.RLock()
|
||||
lastPoll := m.lastPBSBackupPoll[instanceName]
|
||||
m.mu.RUnlock()
|
||||
|
||||
shouldPoll, reason, newLast := m.shouldRunBackupPoll(lastPoll, now)
|
||||
if !shouldPoll {
|
||||
if reason != "" {
|
||||
log.Debug().
|
||||
Str("instance", instanceName).
|
||||
Str("reason", reason).
|
||||
Msg("Skipping PBS backup polling this cycle")
|
||||
}
|
||||
} else {
|
||||
datastoreSnapshot := make([]models.PBSDatastore, len(pbsInst.Datastores))
|
||||
copy(datastoreSnapshot, pbsInst.Datastores)
|
||||
|
||||
go func(ds []models.PBSDatastore) {
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
delete(m.pbsBackupPollers, instanceName)
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
|
||||
log.Info().
|
||||
m.mu.Lock()
|
||||
if m.pbsBackupPollers[instanceName] {
|
||||
m.mu.Unlock()
|
||||
log.Debug().
|
||||
Str("instance", instanceName).
|
||||
Int("datastores", len(ds)).
|
||||
Msg("Starting background PBS backup polling")
|
||||
Msg("PBS backup polling already in progress")
|
||||
} else {
|
||||
m.pbsBackupPollers[instanceName] = true
|
||||
m.lastPBSBackupPoll[instanceName] = newLast
|
||||
m.mu.Unlock()
|
||||
|
||||
backupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
go func(ds []models.PBSDatastore, inst string, start time.Time, pbsClient *pbs.Client) {
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
delete(m.pbsBackupPollers, inst)
|
||||
m.lastPBSBackupPoll[inst] = time.Now()
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
|
||||
m.pollPBSBackups(backupCtx, instanceName, client, ds)
|
||||
}(datastoreSnapshot)
|
||||
log.Info().
|
||||
Str("instance", inst).
|
||||
Int("datastores", len(ds)).
|
||||
Msg("Starting background PBS backup polling")
|
||||
|
||||
backupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
m.pollPBSBackups(backupCtx, inst, pbsClient, ds)
|
||||
|
||||
log.Info().
|
||||
Str("instance", inst).
|
||||
Dur("duration", time.Since(start)).
|
||||
Msg("Completed background PBS backup polling")
|
||||
}(datastoreSnapshot, instanceName, now, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue