Add UPS support. Refactor file namings

This commit is contained in:
Meliox 2026-01-10 19:44:20 +01:00
parent 0d608abf6f
commit db653e4c4b

View File

@ -42,9 +42,11 @@ my $pve_mod_working_dir = '/run/pveproxy/pve-mod';
my $stats_dir = $pve_mod_working_dir;
my $state_file = "$pve_mod_working_dir/stats.json";
my $sensors_state_file = "$pve_mod_working_dir/sensors.json";
my $lock_file = "$pve_mod_working_dir/pve-mod-worker.lock";
my $pve_mod_worker_lock = "$pve_mod_working_dir/pve-mod-pve_mod_worker.lock";
my $startup_lock = $lock_file . ".startup";
my $ups_state_file = "$pve_mod_working_dir/ups.json";
my $collectors_pid_file = "$pve_mod_working_dir/collectors.pids"; # List of collector PIDs
my $pve_mod_worker_lock = "$pve_mod_working_dir/pve-mod-pve_mod_worker.lock"; # PVE Mod worker lock
my $startup_lock = "$pve_mod_working_dir/startup.lock"; # Exclusive startup lock
my $last_snapshot = {};
my $last_mtime = 0;
my $is_collector_parent = 0; # Flag to track if this process started collectors
@ -54,9 +56,15 @@ my $COLLECTOR_TIMEOUT = 10; # Stop collectors x seconds after last get_graphic
my $intel_gpu_enabled = 1; # Set to 0 to disable Intel GPU support
my $amd_gpu_enabled = 0; # Set to 1 to enable AMD GPU support (not yet implemented)
my $nvidia_gpu_enabled = 1; # Set to 1 to enable NVIDIA GPU support (not yet implemented)
my $ups_enabled = 1; # Set to 1 to enable UPS support
my $pve_mod_worker_pid;
my $pve_mod_worker_running = 0;
# UPS Configuration
my $ups_device = {
ups_name => 'ups@192.168.3.2', # Format: upsname[@hostname[:port]]
};
# ============================================================================
# Intel GPU Support
# ============================================================================
@ -603,7 +611,7 @@ sub _get_temperature_sensors {
};
$sensorsData = JSON->new->pretty->encode($enhanced_data);
return $sensorsData;
}
@ -985,11 +993,136 @@ sub _cpu_model_by_package {
return "unknown";
}
# ============================================================================
# UPS Support
# ============================================================================
sub _collector_for_ups {
my ($device) = @_;
$0 = "pve-mod-ups-collector";
_debug(__LINE__, "UPS collector started");
# Set up signal handlers for graceful shutdown
my $shutdown = 0;
$SIG{TERM} = sub {
_debug(__LINE__, "UPS collector received SIGTERM");
$shutdown = 1;
};
$SIG{INT} = sub {
_debug(__LINE__, "UPS collector received SIGINT");
$shutdown = 1;
};
while (!$shutdown) {
my $upsData = _get_ups_status($device->{ups_name});
# Write to ups state file
eval {
open my $ofh, '>', $ups_state_file or die "Failed to open $ups_state_file: $!";
print $ofh $upsData;
close $ofh;
_debug(__LINE__, "Wrote ups data to $ups_state_file");
};
if ($@) {
_debug(__LINE__, "Error writing ups data: $@");
}
sleep 1 unless $shutdown;
}
_debug(__LINE__, "UPS collector shutting down");
exit 0;
}
sub _get_ups_status {
my ($ups_name) = @_;
# upsc upsname[@hostname[:port]]
my $cmd = "upsc $ups_name 2>/dev/null";
_debug(__LINE__, "Running command: $cmd");
# Execute command and capture output
my $output = `$cmd`;
_debug(__LINE__, "upsc command output collected");
my $exit_code = $? >> 8;
_debug(__LINE__, "upsc command exited with code $exit_code");
if ($exit_code != 0) {
_debug(__LINE__, "upsc command failed with exit code $exit_code");
return encode_json({ error => "Failed to execute upsc for $ups_name" });
}
_debug(__LINE__, "upsc command executed successfully");
# Convert upsc output to nested hash structure
my $ups_data = _parse_upsc_output($output);
_debug(__LINE__, "Parsed upsc output for $ups_name");
# Check if we got any data
unless (keys %$ups_data) {
_debug(__LINE__, "No data received from upsc for $ups_name");
return encode_json({ error => "No data from UPS $ups_name" });
}
# Wrap in UPS name structure
my $result = {
$ups_name => $ups_data
};
_debug(__LINE__, "Successfully parsed UPS data for $ups_name");
# Return as pretty JSON
return JSON->new->pretty->canonical->encode($result);
}
sub _parse_upsc_output {
my ($output) = @_;
my $ups_data = {};
_debug(__LINE__, "Parsing upsc output");
eval {
foreach my $line (split /\n/, $output) {
# Skip empty lines and SSL init message
next if $line =~ /^\s*$/;
next if $line =~ /^Init SSL/;
# Parse key-value pairs (format: "key: value")
if ($line =~ /^([^:]+):\s*(.*)$/) {
my ($key, $value) = ($1, $2);
# Trim whitespace
$key =~ s/^\s+|\s+$//g;
$value =~ s/^\s+|\s+$//g;
# Store as flat key-value pairs (no nesting)
# Convert numeric values to numbers, keep strings as strings
if ($value =~ /^-?\d+\.?\d*$/) {
$ups_data->{$key} = $value + 0;
} else {
$ups_data->{$key} = $value;
}
}
}
};
if ($@) {
_debug(__LINE__, "Error parsing upsc output: $@");
}
_debug(__LINE__, "Completed parsing upsc output");
return $ups_data;
}
# ============================================================================
# Supporting functions
# ============================================================================
# Check if a process is alive
sub _is_process_alive {
my ($pid) = @_;
return -d "/proc/$pid";
@ -1226,6 +1359,38 @@ sub get_sensors_stats {
return $sensors_data;
}
sub get_ups_stats {
_debug(__LINE__, "get_ups_stats called");
# Start PVE Mod
_pve_mod_starter();
unless (-f $ups_state_file) {
_debug(__LINE__, "UPS state file does not exist: $ups_state_file");
return {};
}
my $ups_data;
eval {
open my $fh, '<', $ups_state_file or die "Failed to open $ups_state_file: $!";
local $/;
my $json = <$fh>;
close($fh);
$ups_data = $json;
_debug(__LINE__, "Read UPS data, JSON length: " . length($json) . " bytes");
_debug(__LINE__, "Read UPS data from $ups_state_file");
};
if ($@) {
_debug(__LINE__, "Failed to read/parse UPS data: $@");
return {};
}
# Notify pve_mod_worker of activity
_notify_pve_mod_worker();
return $ups_data;
}
# ============================================================================
# Main Collector
# ============================================================================
@ -1241,7 +1406,7 @@ sub _start_graphics_collectors {
}
# Read existing collectors from lock file
my %existing_collectors = _read_collector_lock($lock_file);
my %existing_collectors = _read_collector_lock($collectors_pid_file);
# Generalized device collector management for future AMD/NVIDIA support
my @all_devices;
@ -1307,7 +1472,7 @@ sub _start_graphics_collectors {
my $device_name = $device->{card} // $device->{name} // "device$i";
# Check if collector already running
my $existing_pid = _is_collector_running($device_name, $lock_file);
my $existing_pid = _is_collector_running($device_name, $collectors_pid_file);
if ($existing_pid) {
_debug(__LINE__, "Collector for $type $device_name already running with PID $existing_pid");
push @collector_entries, [$existing_pid, $device_name, $type];
@ -1322,7 +1487,7 @@ sub _start_graphics_collectors {
}
# Write all collector PIDs to lock file
unless (_write_collector_lock($lock_file, @collector_entries)) {
unless (_write_collector_lock($collectors_pid_file, @collector_entries)) {
_debug(__LINE__, "Failed to write lock file, terminating collectors");
foreach my $entry (@collector_entries) {
kill 'TERM', $entry->[0];
@ -1363,7 +1528,7 @@ sub _start_sensors_collector {
}
# Check if already running
my $existing_pid = _is_collector_running('sensors', $lock_file);
my $existing_pid = _is_collector_running('sensors', $collectors_pid_file);
if ($existing_pid) {
_debug(__LINE__, "Sensors collector already running with PID $existing_pid");
return;
@ -1375,7 +1540,7 @@ sub _start_sensors_collector {
if ($pid) {
# Read existing entries
my @collector_entries;
my %existing = _read_collector_lock($lock_file);
my %existing = _read_collector_lock($collectors_pid_file);
foreach my $name (keys %existing) {
push @collector_entries, [$existing{$name}->{pid}, $name, $existing{$name}->{type}];
}
@ -1384,7 +1549,7 @@ sub _start_sensors_collector {
push @collector_entries, [$pid, 'sensors', 'sensors'];
# Write updated lock file
unless (_write_collector_lock($lock_file, @collector_entries)) {
unless (_write_collector_lock($collectors_pid_file, @collector_entries)) {
_debug(__LINE__, "Failed to update lock file, terminating sensors collector");
kill 'TERM', $pid;
return;
@ -1400,6 +1565,59 @@ sub _start_sensors_collector {
}
}
sub _start_ups_collector {
if ($ups_enabled == 0) {
_debug(__LINE__, "UPS support not enabled, skipping collector startup");
return;
}
_debug(__LINE__, "Starting UPS collector");
# Check if UPS is configured
unless (defined $ups_device && $ups_device->{ups_name}) {
_debug(__LINE__, "No UPS configured, skipping collector startup");
return;
}
# Check if already running
my $existing_pid = _is_collector_running('ups', $collectors_pid_file);
if ($existing_pid) {
_debug(__LINE__, "UPS collector already running with PID $existing_pid");
return;
}
# Start the collector
my $pid = _start_child_collector('ups', \&_collector_for_ups, $ups_device);
if ($pid) {
# Read existing entries
my @collector_entries;
my %existing = _read_collector_lock($collectors_pid_file);
foreach my $name (keys %existing) {
push @collector_entries, [$existing{$name}->{pid}, $name, $existing{$name}->{type}];
}
# Add UPS entry
push @collector_entries, [$pid, 'ups', 'ups'];
# Write updated lock file
unless (_write_collector_lock($collectors_pid_file, @collector_entries)) {
_debug(__LINE__, "Failed to update lock file, terminating UPS collector");
kill 'TERM', $pid;
return;
}
# Verify it's alive
sleep 0.1;
if (kill(0, $pid)) {
_debug(__LINE__, "Verified UPS collector (PID $pid) is alive");
} else {
_debug(__LINE__, "WARNING - UPS collector (PID $pid) died immediately!");
}
}
}
# ============================================================================
# PVE Mod Worker
# ============================================================================
@ -1418,12 +1636,22 @@ sub _pve_mod_starter {
# Ensure directory exists
_ensure_pve_mod_directory_exists();
# Try to acquire startup lock FIRST (prevents race conditions)
# Try to get the lock
_debug(__LINE__, "Trying to acquire startup lock: $startup_lock");
my $startup_fh = _acquire_exclusive_lock($startup_lock, 'startup lock');
return unless $startup_fh;
# SECOND CHECK (after lock) - verify nothing changed while waiting
if (_is_pve_mod_worker_running()) {
_debug(__LINE__, "Worker started by another process while we waited for lock");
close($startup_fh);
unlink($startup_lock);
return "already running";
}
# Now we KNOW we're the only one starting things
print $startup_fh "$$\n";
close($startup_fh);
$startup_fh->flush();
_debug(__LINE__, "Wrote PID, $$, to startup lock");
# Start sensors collector first
@ -1432,6 +1660,9 @@ sub _pve_mod_starter {
# Start graphics collectors
_start_graphics_collectors();
# Start UPS collector
_start_ups_collector();
_debug(__LINE__, "All collectors started");
# Start pve mod worker
@ -1444,10 +1675,6 @@ sub _pve_mod_starter {
_debug(__LINE__, "pve_mod_worker started successfully, returning");
}
# ============================================================================
# PVE Mod Worker
# ============================================================================
sub _pve_mod_worker {
_debug(__LINE__, "_pve_mod_worker called");
@ -1577,7 +1804,7 @@ sub _stop_collectors {
# Read current PIDs from lock file
my @pids;
if (open my $lock_fh, '<', $lock_file) {
if (open my $lock_fh, '<', $collectors_pid_file) {
while (my $line = <$lock_fh>) {
chomp $line;
# Extract PID from "PID card type" format
@ -1625,8 +1852,8 @@ sub _stop_collectors {
if (-f $state_file) {
unlink $state_file or _debug(__LINE__, "Failed to remove $state_file: $!");
}
if (-f $lock_file) {
unlink $lock_file or _debug(__LINE__, "Failed to remove $lock_file: $!");
if (-f $collectors_pid_file) {
unlink $collectors_pid_file or _debug(__LINE__, "Failed to remove $collectors_pid_file: $!");
}
# Remove pve mod worker directory and all files if it exists