Madagascar / projects / autoSMART / scripts / autosmart-migration-report.pl
f16725e 3 months ago History
1 contributor
615 lines | 17.089kb
#!/usr/bin/perl

use strict;
use warnings;
use DBI;
use Getopt::Long;
use Config::Simple;
use JSON::XS;
use POSIX qw(strftime);

=head1 NAME

autosmart-migration-report.pl - HDD Migration Analysis and Reporting

=head1 SYNOPSIS

    autosmart-migration-report.pl [OPTIONS]

=head1 OPTIONS

    --config-dir DIR     Configuration directory (default: /etc/pve/autoSMART)
    --days N            Days of migration history (default: 30)
    --serial SERIAL     Report for specific HDD serial number
    --node NODE         Report migrations for specific node
    --type TYPE         Migration type: device_change, node_change, slot_change, all
    --format FORMAT     Output format: text, json, csv (default: text)
    --frequent-only     Show only frequently migrated drives (>3 migrations)
    --recent-only       Show only recent migrations (<24h)
    --output FILE       Write to file instead of stdout
    --help              Show this help

=head1 DESCRIPTION

Analyze and report HDD migrations tracked by autoSMART. Shows drive movements
between nodes, device path changes, and slot changes with detailed history.

=cut

# Configuration
my $config_dir = '/etc/pve/autoSMART';
my $days = 30;
my $specific_serial = '';
my $specific_node = '';
my $migration_type = 'all';
my $format = 'text';
my $frequent_only = 0;
my $recent_only = 0;
my $output_file = '';
my $help = 0;

GetOptions(
    'config-dir=s'  => \$config_dir,
    'days=i'        => \$days,
    'serial=s'      => \$specific_serial,
    'node=s'        => \$specific_node,
    'type=s'        => \$migration_type,
    'format=s'      => \$format,
    'frequent-only' => \$frequent_only,
    'recent-only'   => \$recent_only,
    'output=s'      => \$output_file,
    'help'          => \$help,
) or die "Error parsing command line arguments\n";

if ($help) {
    print_help();
    exit 0;
}

# Validate options
unless ($format =~ /^(text|json|csv)$/) {
    die "Invalid format: $format (must be text, json, or csv)\n";
}

unless ($migration_type =~ /^(device_change|node_change|slot_change|all)$/) {
    die "Invalid migration type: $migration_type\n";
}

# Connect to database
my $db_config = "$config_dir/cluster.conf";
unless (-f $db_config) {
    die "Cluster configuration not found: $db_config\n";
}

my $cfg = Config::Simple->new($db_config);
my $dsn = sprintf("DBI:Pg:database=%s;host=%s;port=%s",
    $cfg->param('database.database'),
    $cfg->param('database.host'),
    $cfg->param('database.port')
);

my $dbh = DBI->connect(
    $dsn,
    $cfg->param('database.username'),
    $cfg->param('database.password'),
    { RaiseError => 1, AutoCommit => 1, pg_enable_utf8 => 1 }
) or die "Database connection failed: $DBI::errstr";

# Generate migration report
my $report_data = generate_migration_report($dbh);

# Output report
my $output_handle = \*STDOUT;
if ($output_file) {
    open $output_handle, '>', $output_file 
        or die "Cannot open output file $output_file: $!\n";
}

if ($format eq 'json') {
    output_json($output_handle, $report_data);
} elsif ($format eq 'csv') {
    output_csv($output_handle, $report_data);
} else {
    output_text($output_handle, $report_data);
}

close $output_handle if $output_file;
$dbh->disconnect();

=head2 generate_migration_report

Generate comprehensive migration report

=cut

sub generate_migration_report {
    my $dbh = shift;
    
    my $data = {
        generated_at => time(),
        days_analyzed => $days,
        filters => {
            serial => $specific_serial,
            node => $specific_node,
            type => $migration_type,
            frequent_only => $frequent_only,
            recent_only => $recent_only,
        }
    };
    
    # Get migration statistics
    $data->{statistics} = get_migration_statistics($dbh);
    
    # Get migration details
    $data->{migrations} = get_migration_details($dbh);
    
    # Get frequently migrated drives
    $data->{frequent_migrants} = get_frequent_migrants($dbh);
    
    # Get drive current status
    $data->{drive_status} = get_drive_migration_status($dbh);
    
    return $data;
}

=head2 get_migration_statistics

Get overall migration statistics

=cut

sub get_migration_statistics {
    my $dbh = shift;
    
    my $stats = {};
    
    # Total migrations by type
    my $sql = q{
        SELECT 
            migration_type,
            COUNT(*) as count,
            COUNT(DISTINCT serial_number) as unique_drives
        FROM hdd_migrations
        WHERE migration_timestamp >= NOW() - INTERVAL ? DAY
        GROUP BY migration_type
        ORDER BY count DESC
    };
    
    my $sth = $dbh->prepare($sql);
    $sth->execute($days);
    
    $stats->{by_type} = {};
    while (my $row = $sth->fetchrow_hashref()) {
        $stats->{by_type}->{$row->{migration_type}} = {
            count => $row->{count},
            unique_drives => $row->{unique_drives}
        };
    }
    
    # Migrations by node
    $sql = q{
        SELECT 
            COALESCE(new_node_id, old_node_id) as node_id,
            COUNT(*) as migrations_involving_node
        FROM hdd_migrations
        WHERE migration_timestamp >= NOW() - INTERVAL ? DAY
        GROUP BY COALESCE(new_node_id, old_node_id)
        ORDER BY migrations_involving_node DESC
    };
    
    $sth = $dbh->prepare($sql);
    $sth->execute($days);
    
    $stats->{by_node} = {};
    while (my $row = $sth->fetchrow_hashref()) {
        $stats->{by_node}->{$row->{node_id}} = $row->{migrations_involving_node};
    }
    
    # Recent activity
    $sql = q{
        SELECT 
            DATE(migration_timestamp) as date,
            COUNT(*) as migrations_per_day
        FROM hdd_migrations
        WHERE migration_timestamp >= NOW() - INTERVAL ? DAY
        GROUP BY DATE(migration_timestamp)
        ORDER BY date DESC
        LIMIT 7
    };
    
    $sth = $dbh->prepare($sql);
    $sth->execute($days);
    
    $stats->{recent_activity} = [];
    while (my $row = $sth->fetchrow_hashref()) {
        push @{$stats->{recent_activity}}, {
            date => $row->{date},
            count => $row->{migrations_per_day}
        };
    }
    
    return $stats;
}

=head2 get_migration_details

Get detailed migration records

=cut

sub get_migration_details {
    my $dbh = shift;
    
    my $sql = q{
        SELECT 
            m.serial_number,
            hi.model_name,
            hi.current_device_path,
            hi.current_node_id,
            m.migration_type,
            m.migration_timestamp,
            m.old_device_path,
            m.old_node_id,
            m.old_slot,
            m.new_device_path,
            m.new_node_id,
            m.new_slot,
            m.detected_by,
            m.confidence_level,
            m.trigger_reason,
            m.verification_status
        FROM hdd_migrations m
        JOIN hdd_inventory hi ON m.hdd_id = hi.id
        WHERE m.migration_timestamp >= NOW() - INTERVAL ? DAY
    };
    
    my @params = ($days);
    
    # Add filters
    if ($specific_serial) {
        $sql .= " AND m.serial_number = ?";
        push @params, $specific_serial;
    }
    
    if ($specific_node) {
        $sql .= " AND (m.old_node_id = ? OR m.new_node_id = ?)";
        push @params, $specific_node, $specific_node;
    }
    
    if ($migration_type ne 'all') {
        $sql .= " AND m.migration_type = ?";
        push @params, $migration_type;
    }
    
    if ($recent_only) {
        $sql .= " AND m.migration_timestamp >= NOW() - INTERVAL '24 hours'";
    }
    
    $sql .= " ORDER BY m.migration_timestamp DESC LIMIT 100";
    
    my $sth = $dbh->prepare($sql);
    $sth->execute(@params);
    
    my @migrations = ();
    while (my $row = $sth->fetchrow_hashref()) {
        push @migrations, $row;
    }
    
    return \@migrations;
}

=head2 get_frequent_migrants

Get drives that migrate frequently

=cut

sub get_frequent_migrants {
    my $dbh = shift;
    
    my $min_migrations = $frequent_only ? 3 : 1;
    
    my $sql = q{
        SELECT 
            hi.serial_number,
            hi.model_name,
            hi.current_device_path,
            hi.current_node_id,
            hi.migration_count,
            hi.last_migration,
            hi.first_seen,
            COUNT(m.id) as recent_migrations,
            string_agg(DISTINCT m.migration_type, ', ') as migration_types
        FROM hdd_inventory hi
        LEFT JOIN hdd_migrations m ON hi.id = m.hdd_id
            AND m.migration_timestamp >= NOW() - INTERVAL ? DAY
        WHERE hi.migration_count >= ?
        GROUP BY hi.id, hi.serial_number, hi.model_name, hi.current_device_path,
                 hi.current_node_id, hi.migration_count, hi.last_migration, hi.first_seen
        HAVING COUNT(m.id) > 0 OR hi.migration_count >= ?
        ORDER BY hi.migration_count DESC, hi.last_migration DESC
        LIMIT 20
    };
    
    my $sth = $dbh->prepare($sql);
    $sth->execute($days, $min_migrations, $min_migrations);
    
    my @frequent = ();
    while (my $row = $sth->fetchrow_hashref()) {
        push @frequent, $row;
    }
    
    return \@frequent;
}

=head2 get_drive_migration_status

Get current migration status of drives

=cut

sub get_drive_migration_status {
    my $dbh = shift;
    
    my $sql = q{
        SELECT 
            migration_status,
            COUNT(*) as drive_count
        FROM drive_health_summary
        GROUP BY migration_status
        ORDER BY drive_count DESC
    };
    
    my $sth = $dbh->prepare($sql);
    $sth->execute();
    
    my %status = ();
    while (my $row = $sth->fetchrow_hashref()) {
        $status{$row->{migration_status}} = $row->{drive_count};
    }
    
    return \%status;
}

=head2 output_text

Output report as text

=cut

sub output_text {
    my ($fh, $data) = @_;
    
    print $fh "\n" . "="x80 . "\n";
    print $fh "autoSMART HDD Migration Report\n";
    print $fh "Generated: " . strftime("%Y-%m-%d %H:%M:%S", localtime($data->{generated_at})) . "\n";
    print $fh "Period: Last $data->{days_analyzed} days\n";
    print $fh "="x80 . "\n\n";
    
    # Statistics
    my $stats = $data->{statistics};
    
    print $fh "MIGRATION STATISTICS\n";
    print $fh "-"x40 . "\n";
    
    if (%{$stats->{by_type}}) {
        print $fh "By Type:\n";
        foreach my $type (sort keys %{$stats->{by_type}}) {
            my $info = $stats->{by_type}->{$type};
            printf $fh "  %-15s: %d migrations (%d unique drives)\n",
                   $type, $info->{count}, $info->{unique_drives};
        }
    }
    
    if (%{$stats->{by_node}}) {
        print $fh "\nBy Node:\n";
        foreach my $node (sort { $stats->{by_node}->{$b} <=> $stats->{by_node}->{$a} } 
                         keys %{$stats->{by_node}}) {
            printf $fh "  %-15s: %d migrations\n", $node, $stats->{by_node}->{$node};
        }
    }
    
    # Recent activity
    if (@{$stats->{recent_activity}}) {
        print $fh "\nRecent Activity (Last 7 days):\n";
        foreach my $activity (@{$stats->{recent_activity}}) {
            printf $fh "  %s: %d migrations\n", $activity->{date}, $activity->{count};
        }
    }
    
    # Drive migration status
    if (%{$data->{drive_status}}) {
        print $fh "\nDrive Migration Status:\n";
        foreach my $status (sort keys %{$data->{drive_status}}) {
            printf $fh "  %-20s: %d drives\n", $status, $data->{drive_status}->{$status};
        }
    }
    
    # Frequently migrated drives
    if (@{$data->{frequent_migrants}}) {
        print $fh "\n" . "="x80 . "\n";
        print $fh "FREQUENTLY MIGRATED DRIVES\n";
        print $fh "="x80 . "\n";
        
        foreach my $drive (@{$data->{frequent_migrants}}) {
            printf $fh "\nSerial: %s (%s)\n", 
                   $drive->{serial_number}, $drive->{model_name};
            printf $fh "Current: %s @ %s\n", 
                   $drive->{current_device_path} || 'unknown',
                   $drive->{current_node_id} || 'unknown';
            printf $fh "Total migrations: %d (Recent: %d)\n", 
                   $drive->{migration_count}, $drive->{recent_migrations};
            printf $fh "Last migration: %s\n", 
                   $drive->{last_migration} || 'never';
            printf $fh "Migration types: %s\n", 
                   $drive->{migration_types} || 'none';
        }
    }
    
    # Recent migrations
    if (@{$data->{migrations}}) {
        print $fh "\n" . "="x80 . "\n";
        print $fh "RECENT MIGRATIONS\n";
        print $fh "="x80 . "\n";
        
        foreach my $migration (@{$data->{migrations}}) {
            printf $fh "\n[%s] %s - %s\n",
                   $migration->{migration_timestamp},
                   $migration->{serial_number},
                   uc($migration->{migration_type});
            
            if ($migration->{migration_type} eq 'node_change') {
                printf $fh "  Moved: %s@%s -> %s@%s\n",
                       $migration->{old_device_path} || '?',
                       $migration->{old_node_id} || '?',
                       $migration->{new_device_path} || '?',
                       $migration->{new_node_id} || '?';
            } elsif ($migration->{migration_type} eq 'device_change') {
                printf $fh "  Device: %s -> %s (on %s)\n",
                       $migration->{old_device_path} || '?',
                       $migration->{new_device_path} || '?',
                       $migration->{new_node_id} || '?';
            }
            
            printf $fh "  Detected by: %s (confidence: %d/10)\n",
                   $migration->{detected_by}, $migration->{confidence_level};
            
            if ($migration->{trigger_reason}) {
                printf $fh "  Reason: %s\n", $migration->{trigger_reason};
            }
        }
    }
    
    print $fh "\n";
}

=head2 output_json

Output report as JSON

=cut

sub output_json {
    my ($fh, $data) = @_;
    
    my $json = JSON::XS->new->pretty->encode($data);
    print $fh $json;
}

=head2 output_csv

Output migrations as CSV

=cut

sub output_csv {
    my ($fh, $data) = @_;
    
    # CSV header
    print $fh "timestamp,serial_number,model_name,migration_type,old_location,new_location,detected_by,confidence\n";
    
    foreach my $migration (@{$data->{migrations}}) {
        my @fields = (
            $migration->{migration_timestamp},
            $migration->{serial_number},
            $migration->{model_name} || '',
            $migration->{migration_type},
            sprintf("%s@%s", $migration->{old_device_path} || '', $migration->{old_node_id} || ''),
            sprintf("%s@%s", $migration->{new_device_path} || '', $migration->{new_node_id} || ''),
            $migration->{detected_by},
            $migration->{confidence_level}
        );
        
        # Escape CSV fields
        @fields = map { escape_csv($_) } @fields;
        print $fh join(',', @fields) . "\n";
    }
}

=head2 escape_csv

Escape CSV field

=cut

sub escape_csv {
    my $field = shift || '';
    
    if ($field =~ /[",\n]/) {
        $field =~ s/"/""/g;
        $field = "\"$field\"";
    }
    
    return $field;
}

=head2 print_help

Display help information

=cut

sub print_help {
    print <<'EOF';
autoSMART HDD Migration Report v1.0

USAGE:
    autosmart-migration-report.pl [OPTIONS]

OPTIONS:
    --config-dir DIR     Configuration directory (default: /etc/pve/autoSMART)
    --days N            Days of migration history to analyze (default: 30)
    --serial SERIAL     Report for specific HDD serial number
    --node NODE         Show migrations involving specific node
    --type TYPE         Filter by migration type:
                        device_change, node_change, slot_change, all (default)
    --format FORMAT     Output format: text, json, csv (default: text)
    --frequent-only     Show only frequently migrated drives (3+ migrations)
    --recent-only       Show only migrations in last 24 hours
    --output FILE       Write to file instead of stdout
    --help              Show this help message

EXAMPLES:
    # Show all migrations in last 7 days
    autosmart-migration-report.pl --days 7

    # Show only node changes
    autosmart-migration-report.pl --type node_change

    # Show migrations for specific drive
    autosmart-migration-report.pl --serial WD-WCC4N5123456

    # Show frequently migrated drives
    autosmart-migration-report.pl --frequent-only

    # Export recent migrations as CSV
    autosmart-migration-report.pl --recent-only --format csv --output migrations.csv

MIGRATION TYPES:
    device_change   Drive appeared at different /dev/sdX path
    node_change     Drive moved between Proxmox nodes
    slot_change     Drive moved to different physical slot/bay
    discovery       New drive detected for first time

OUTPUT:
    The report includes:
    - Overall migration statistics
    - Frequently migrated drives
    - Recent migration activity
    - Detailed migration logs
    - Drive migration status summary

EOF
}

__END__

=head1 AUTHOR

AutoSMART Development Team

=head1 LICENSE

This software is part of the autoSMART project.

=cut