#!/usr/bin/perl use strict; use warnings; use FindBin qw($Bin); use lib "$Bin/../lib"; use SmartCollector; use Getopt::Long; use POSIX qw(strftime); =head1 NAME autosmart-collector.pl - SMART data collection daemon for Proxmox cluster =head1 SYNOPSIS autosmart-collector.pl [OPTIONS] =head1 OPTIONS --cluster-config FILE Cluster configuration file (default: /etc/pve/autoSMART/cluster.conf) --local-config FILE Local configuration file (default: /etc/default/autosmart) --daemon Run as daemon --once Run once and exit (for cron jobs) --device PATH Collect from specific device only --debug Enable debug logging --help Show this help =head1 DESCRIPTION This script collects SMART data from HDDs in a Proxmox cluster environment. Configuration is split between cluster-wide settings in /etc/pve/autoSMART/ and local node settings in /etc/default/autosmart. =cut # Configuration my $cluster_config = '/etc/pve/autoSMART/cluster.conf'; my $local_config = '/etc/default/autosmart'; my $daemon_mode = 0; my $run_once = 0; my $specific_device = ''; my $debug = 0; my $help = 0; GetOptions( 'cluster-config=s' => \$cluster_config, 'local-config=s' => \$local_config, 'daemon' => \$daemon_mode, 'once' => \$run_once, 'device=s' => \$specific_device, 'debug' => \$debug, 'help' => \$help, ) or die "Error parsing command line arguments\n"; if ($help) { print_help(); exit 0; } # Load local configuration for environment setup my %local_settings = load_local_config($local_config); # Override debug flag from local config if not specified unless ($debug) { $debug = ($local_settings{AUTOSMART_DEBUG_ENABLED} eq 'true') ? ($local_settings{AUTOSMART_DEBUG_LEVEL} || 1) : 0; } # Validate configuration files unless (-f $cluster_config) { die "Cluster configuration not found: $cluster_config\n"; } unless (-f $local_config) { die "Local configuration not found: $local_config\n"; } # Check for emergency stop if (-f ($local_settings{AUTOSMART_EMERGENCY_STOP_FILE} || '/etc/autosmart/EMERGENCY_STOP')) { die "Emergency stop file detected - autoSMART is disabled\n"; } # Initialize collector with Proxmox cluster configuration my $collector = SmartCollector->new( cluster_config => $cluster_config, local_config => $local_config, debug => $debug, ); log_message("autoSMART collector starting for cluster node..."); if ($specific_device) { # Collect from specific device collect_specific_device($collector, $specific_device); } elsif ($run_once) { # Single collection run run_collection_cycle($collector); } elsif ($daemon_mode) { # Daemon mode run_daemon($collector, \%local_settings); } else { # Default: single collection run run_collection_cycle($collector); } log_message("autoSMART collector finished"); =head2 load_local_config Load local configuration from /etc/default/autosmart =cut sub load_local_config { my $config_file = shift; my %settings = (); return %settings unless -f $config_file; open my $fh, '<', $config_file or die "Cannot read local config: $config_file: $!"; while (my $line = <$fh>) { chomp $line; next if $line =~ /^\s*#/ || $line =~ /^\s*$/; if ($line =~ /^(\w+)=(.+)$/) { my ($key, $value) = ($1, $2); $value =~ s/^["']|["']$//g; # Remove quotes $settings{$key} = $value; } } close $fh; return %settings; } =head2 collect_specific_device Collect SMART data from a specific device =cut sub collect_specific_device { my ($collector, $device_path) = @_; log_message("Collecting SMART data from $device_path"); my $smart_data = $collector->collect_smart_data($device_path); unless ($smart_data) { log_message("ERROR: Failed to collect SMART data from $device_path"); exit 1; } # Create minimal drive info for storage my $drive_info = { device_path => $device_path, serial => $smart_data->{serial_number} || 'unknown', model => $smart_data->{model_name} || 'unknown', size_gb => 0, madagascar_id => "manual_$device_path", }; if ($collector->store_smart_data($drive_info, $smart_data)) { log_message("Successfully stored SMART data for $device_path"); } else { log_message("ERROR: Failed to store SMART data for $device_path"); exit 1; } } =head2 run_collection_cycle Execute one complete collection cycle =cut sub run_collection_cycle { my $collector = shift; log_message("Starting collection cycle"); my $result = $collector->collect_all(); log_message(sprintf( "Collection cycle complete: %d successful, %d failed, %d total", $result->{successful}, $result->{failed}, $result->{total} )); # Exit with error code if any collections failed if ($result->{failed} > 0) { exit 1; } } =head2 run_daemon Run as daemon with periodic collection =cut sub run_daemon { my $collector = shift; # Get collection interval from config my $cfg = Config::Simple->new("$config_dir/smart.conf"); my $interval = $cfg->param('monitoring.collection_interval') || 300; log_message("Running in daemon mode (interval: ${interval}s)"); # Set up signal handlers for graceful shutdown my $running = 1; $SIG{TERM} = sub { log_message("Received SIGTERM, shutting down gracefully"); $running = 0; }; $SIG{INT} = sub { log_message("Received SIGINT, shutting down gracefully"); $running = 0; }; # Main daemon loop while ($running) { my $start_time = time(); eval { run_collection_cycle($collector); }; if ($@) { log_message("ERROR in collection cycle: $@"); } # Calculate sleep time to maintain interval my $elapsed = time() - $start_time; my $sleep_time = $interval - $elapsed; if ($sleep_time > 0) { log_message("Sleeping for ${sleep_time}s until next collection"); # Sleep in small chunks to allow signal handling while ($sleep_time > 0 && $running) { my $chunk = $sleep_time > 5 ? 5 : $sleep_time; sleep($chunk); $sleep_time -= $chunk; } } else { log_message("WARNING: Collection took longer than interval (${elapsed}s > ${interval}s)"); } } log_message("Daemon shutdown complete"); } =head2 log_message Log message with timestamp =cut sub log_message { my $message = shift; my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime()); print "[$timestamp] autosmart-collector: $message\n"; } =head2 print_help Display help information =cut sub print_help { print <<'EOF'; autoSMART Data Collector v1.0 USAGE: autosmart-collector.pl [OPTIONS] OPTIONS: --config-dir DIR Configuration directory (default: /etc/autosmart) --daemon Run as daemon with periodic collection --once Run once and exit (useful for cron jobs) --device PATH Collect from specific device only (e.g., /dev/sda) --debug Enable debug logging --help Show this help message EXAMPLES: # Run once (for cron jobs) autosmart-collector.pl --once # Run as daemon autosmart-collector.pl --daemon # Collect from specific device autosmart-collector.pl --device /dev/sda # Run with debug logging autosmart-collector.pl --debug --once # Use custom config directory autosmart-collector.pl --config-dir /opt/autosmart/config --once CONFIGURATION: Configuration files should be in /etc/autosmart/ or specified directory: - smart.conf SMART monitoring settings - database.conf PostgreSQL connection settings DAEMON MODE: In daemon mode, the collector runs continuously and collects data at intervals specified in smart.conf (monitoring.collection_interval). Send SIGTERM or SIGINT for graceful shutdown. CRON MODE: Use --once flag for cron-based scheduling: # Collect every 5 minutes */5 * * * * /usr/local/bin/autosmart-collector.pl --once EXIT CODES: 0 Success 1 Error (failed collections, missing config, etc.) EOF } __END__ =head1 AUTHOR AutoSMART Development Team =head1 LICENSE This software is part of the autoSMART project. =cut