Newer Older
f16725e 3 months ago History
662 lines | 17.463kb
Bogdan Timofte authored 3 months ago
1
#!/usr/bin/perl
2

            
3
use strict;
4
use warnings;
5
use DBI;
6
use Getopt::Long;
7
use Config::Simple;
8
use JSON::XS;
9
use POSIX qw(strftime);
10

            
11
=head1 NAME
12

            
13
autosmart-report.pl - Generate comprehensive reports for autoSMART system
14

            
15
=head1 SYNOPSIS
16

            
17
    autosmart-report.pl [OPTIONS]
18

            
19
=head1 OPTIONS
20

            
21
    --config-dir DIR     Configuration directory (default: /etc/autosmart)
22
    --report TYPE        Report type: summary, detailed, health, alerts, trends
23
    --device PATH        Report for specific device only
24
    --days N            Days of history to include (default: 30)
25
    --format FORMAT     Output format: text, html, json (default: text)
26
    --output FILE       Write to file instead of stdout
27
    --help              Show this help
28

            
29
=head1 DESCRIPTION
30

            
31
Generate various reports from autoSMART data including drive health summaries,
32
detailed SMART analysis, alert history, and trend analysis.
33

            
34
=cut
35

            
36
# Configuration
37
my $config_dir = '/etc/autosmart';
38
my $report_type = 'summary';
39
my $specific_device = '';
40
my $days = 30;
41
my $format = 'text';
42
my $output_file = '';
43
my $help = 0;
44

            
45
GetOptions(
46
    'config-dir=s' => \$config_dir,
47
    'report=s'     => \$report_type,
48
    'device=s'     => \$specific_device,
49
    'days=i'       => \$days,
50
    'format=s'     => \$format,
51
    'output=s'     => \$output_file,
52
    'help'         => \$help,
53
) or die "Error parsing command line arguments\n";
54

            
55
if ($help) {
56
    print_help();
57
    exit 0;
58
}
59

            
60
# Validate options
61
unless ($report_type =~ /^(summary|detailed|health|alerts|trends)$/) {
62
    die "Invalid report type: $report_type\n";
63
}
64

            
65
unless ($format =~ /^(text|html|json)$/) {
66
    die "Invalid format: $format\n";
67
}
68

            
69
# Connect to database
70
my $db_config = "$config_dir/database.conf";
71
unless (-f $db_config) {
72
    die "Database configuration not found: $db_config\n";
73
}
74

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

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

            
89
# Generate report
90
my $report_data = generate_report($dbh, $report_type, $specific_device, $days);
91

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

            
99
if ($format eq 'json') {
100
    output_json($output_handle, $report_data);
101
} elsif ($format eq 'html') {
102
    output_html($output_handle, $report_data, $report_type);
103
} else {
104
    output_text($output_handle, $report_data, $report_type);
105
}
106

            
107
close $output_handle if $output_file;
108

            
109
$dbh->disconnect();
110

            
111
=head2 generate_report
112

            
113
Generate report data based on type
114

            
115
=cut
116

            
117
sub generate_report {
118
    my ($dbh, $type, $device, $days) = @_;
119

            
120
    my $data = {
121
        report_type => $type,
122
        generated_at => time(),
123
        days_included => $days,
124
        specific_device => $device,
125
    };
126

            
127
    if ($type eq 'summary') {
128
        $data->{summary} = get_system_summary($dbh, $days);
129
    } elsif ($type eq 'detailed') {
130
        $data->{drives} = get_detailed_drive_info($dbh, $device, $days);
131
    } elsif ($type eq 'health') {
132
        $data->{health} = get_health_overview($dbh, $device);
133
    } elsif ($type eq 'alerts') {
134
        $data->{alerts} = get_alert_history($dbh, $device, $days);
135
    } elsif ($type eq 'trends') {
136
        $data->{trends} = get_trend_analysis($dbh, $device, $days);
137
    }
138

            
139
    return $data;
140
}
141

            
142
=head2 get_system_summary
143

            
144
Get high-level system summary
145

            
146
=cut
147

            
148
sub get_system_summary {
149
    my ($dbh, $days) = @_;
150

            
151
    my $summary = {};
152

            
153
    # Drive counts by status
154
    my $sth = $dbh->prepare(q{
155
        SELECT status, COUNT(*) as count
156
        FROM hdd_inventory
157
        GROUP BY status
158
    });
159
    $sth->execute();
160

            
161
    $summary->{drive_counts} = {};
162
    while (my $row = $sth->fetchrow_hashref()) {
163
        $summary->{drive_counts}->{$row->{status}} = $row->{count};
164
    }
165

            
166
    # Recent predictions summary
167
    $sth = $dbh->prepare(q{
168
        SELECT risk_level, COUNT(*) as count
169
        FROM predictions
170
        WHERE timestamp >= NOW() - INTERVAL ? DAY
171
        GROUP BY risk_level
172
    });
173
    $sth->execute($days);
174

            
175
    $summary->{recent_predictions} = {};
176
    while (my $row = $sth->fetchrow_hashref()) {
177
        $summary->{recent_predictions}->{$row->{risk_level}} = $row->{count};
178
    }
179

            
180
    # Recent alerts
181
    $sth = $dbh->prepare(q{
182
        SELECT alert_type, COUNT(*) as count
183
        FROM alert_history
184
        WHERE sent_at >= NOW() - INTERVAL ? DAY
185
        GROUP BY alert_type
186
    });
187
    $sth->execute($days);
188

            
189
    $summary->{recent_alerts} = {};
190
    while (my $row = $sth->fetchrow_hashref()) {
191
        $summary->{recent_alerts}->{$row->{alert_type}} = $row->{count};
192
    }
193

            
194
    # Data collection stats
195
    $sth = $dbh->prepare(q{
196
        SELECT
197
            COUNT(*) as total_readings,
198
            COUNT(DISTINCT device_path) as devices_with_data,
199
            AVG(CASE WHEN collection_ok THEN 1.0 ELSE 0.0 END) * 100 as success_rate
200
        FROM smart_readings
201
        WHERE timestamp >= NOW() - INTERVAL ? DAY
202
    });
203
    $sth->execute($days);
204

            
205
    if (my $row = $sth->fetchrow_hashref()) {
206
        $summary->{collection_stats} = {
207
            total_readings => $row->{total_readings},
208
            devices_with_data => $row->{devices_with_data},
209
            success_rate => sprintf("%.1f", $row->{success_rate} || 0),
210
        };
211
    }
212

            
213
    return $summary;
214
}
215

            
216
=head2 get_detailed_drive_info
217

            
218
Get detailed information for drives
219

            
220
=cut
221

            
222
sub get_detailed_drive_info {
223
    my ($dbh, $device, $days) = @_;
224

            
225
    my $sql = q{
226
        SELECT
227
            hi.device_path,
228
            hi.model_name,
229
            hi.serial_number,
230
            hi.size_gb,
231
            hi.status,
232
            hi.first_seen,
233
            hi.last_seen,
234
            COUNT(sr.id) as reading_count,
235
            AVG(sr.temperature) as avg_temperature,
236
            MAX(sr.temperature) as max_temperature
237
        FROM hdd_inventory hi
238
        LEFT JOIN smart_readings sr ON hi.device_path = sr.device_path
239
            AND sr.timestamp >= NOW() - INTERVAL ? DAY
240
    };
241

            
242
    my @params = ($days);
243

            
244
    if ($device) {
245
        $sql .= " WHERE hi.device_path = ?";
246
        push @params, $device;
247
    }
248

            
249
    $sql .= q{
250
        GROUP BY hi.device_path, hi.model_name, hi.serial_number,
251
                 hi.size_gb, hi.status, hi.first_seen, hi.last_seen
252
        ORDER BY hi.device_path
253
    };
254

            
255
    my $sth = $dbh->prepare($sql);
256
    $sth->execute(@params);
257

            
258
    my @drives = ();
259
    while (my $row = $sth->fetchrow_hashref()) {
260
        # Get latest prediction
261
        my $pred_sth = $dbh->prepare(q{
262
            SELECT risk_level, confidence, timestamp
263
            FROM predictions
264
            WHERE device_path = ?
265
            ORDER BY timestamp DESC
266
            LIMIT 1
267
        });
268
        $pred_sth->execute($row->{device_path});
269

            
270
        if (my $pred = $pred_sth->fetchrow_hashref()) {
271
            $row->{latest_prediction} = $pred;
272
        }
273

            
274
        # Get recent alerts
275
        my $alert_sth = $dbh->prepare(q{
276
            SELECT COUNT(*) as alert_count
277
            FROM alert_history
278
            WHERE device_path = ?
279
            AND sent_at >= NOW() - INTERVAL ? DAY
280
        });
281
        $alert_sth->execute($row->{device_path}, $days);
282

            
283
        if (my $alert = $alert_sth->fetchrow_hashref()) {
284
            $row->{recent_alert_count} = $alert->{alert_count};
285
        }
286

            
287
        push @drives, $row;
288
    }
289

            
290
    return \@drives;
291
}
292

            
293
=head2 get_health_overview
294

            
295
Get current health overview
296

            
297
=cut
298

            
299
sub get_health_overview {
300
    my ($dbh, $device) = @_;
301

            
302
    my $sql = q{
303
        SELECT * FROM drive_health_summary
304
    };
305

            
306
    my @params = ();
307
    if ($device) {
308
        $sql .= " WHERE device_path = ?";
309
        push @params, $device;
310
    }
311

            
312
    $sql .= " ORDER BY device_path";
313

            
314
    my $sth = $dbh->prepare($sql);
315
    $sth->execute(@params);
316

            
317
    my @health_data = ();
318
    while (my $row = $sth->fetchrow_hashref()) {
319
        push @health_data, $row;
320
    }
321

            
322
    return \@health_data;
323
}
324

            
325
=head2 get_alert_history
326

            
327
Get alert history
328

            
329
=cut
330

            
331
sub get_alert_history {
332
    my ($dbh, $device, $days) = @_;
333

            
334
    my $sql = q{
335
        SELECT
336
            ah.device_path,
337
            ah.alert_type,
338
            ah.risk_level,
339
            ah.message,
340
            ah.sent_at,
341
            ah.acknowledged,
342
            ah.acknowledged_by,
343
            hi.model_name
344
        FROM alert_history ah
345
        JOIN hdd_inventory hi ON ah.device_path = hi.device_path
346
        WHERE ah.sent_at >= NOW() - INTERVAL ? DAY
347
    };
348

            
349
    my @params = ($days);
350

            
351
    if ($device) {
352
        $sql .= " AND ah.device_path = ?";
353
        push @params, $device;
354
    }
355

            
356
    $sql .= " ORDER BY ah.sent_at DESC";
357

            
358
    my $sth = $dbh->prepare($sql);
359
    $sth->execute(@params);
360

            
361
    my @alerts = ();
362
    while (my $row = $sth->fetchrow_hashref()) {
363
        push @alerts, $row;
364
    }
365

            
366
    return \@alerts;
367
}
368

            
369
=head2 get_trend_analysis
370

            
371
Get trend analysis data
372

            
373
=cut
374

            
375
sub get_trend_analysis {
376
    my ($dbh, $device, $days) = @_;
377

            
378
    # This is a simplified trend analysis
379
    # In production, you might want more sophisticated analysis
380

            
381
    my $sql = q{
382
        SELECT
383
            device_path,
384
            DATE(timestamp) as date,
385
            AVG(temperature) as avg_temp,
386
            COUNT(*) as reading_count
387
        FROM smart_readings
388
        WHERE timestamp >= NOW() - INTERVAL ? DAY
389
    };
390

            
391
    my @params = ($days);
392

            
393
    if ($device) {
394
        $sql .= " AND device_path = ?";
395
        push @params, $device;
396
    }
397

            
398
    $sql .= q{
399
        GROUP BY device_path, DATE(timestamp)
400
        ORDER BY device_path, date
401
    };
402

            
403
    my $sth = $dbh->prepare($sql);
404
    $sth->execute(@params);
405

            
406
    my %trends = ();
407
    while (my $row = $sth->fetchrow_hashref()) {
408
        push @{$trends{$row->{device_path}}}, {
409
            date => $row->{date},
410
            avg_temp => sprintf("%.1f", $row->{avg_temp} || 0),
411
            reading_count => $row->{reading_count},
412
        };
413
    }
414

            
415
    return \%trends;
416
}
417

            
418
=head2 output_text
419

            
420
Output report as text
421

            
422
=cut
423

            
424
sub output_text {
425
    my ($fh, $data, $type) = @_;
426

            
427
    print $fh "\n" . "="x80 . "\n";
428
    print $fh "autoSMART System Report - " . ucfirst($type) . "\n";
429
    print $fh "Generated: " . strftime("%Y-%m-%d %H:%M:%S", localtime($data->{generated_at})) . "\n";
430
    print $fh "Time Period: Last $data->{days_included} days\n";
431
    print $fh "="x80 . "\n\n";
432

            
433
    if ($type eq 'summary') {
434
        output_summary_text($fh, $data->{summary});
435
    } elsif ($type eq 'detailed') {
436
        output_detailed_text($fh, $data->{drives});
437
    } elsif ($type eq 'health') {
438
        output_health_text($fh, $data->{health});
439
    } elsif ($type eq 'alerts') {
440
        output_alerts_text($fh, $data->{alerts});
441
    } elsif ($type eq 'trends') {
442
        output_trends_text($fh, $data->{trends});
443
    }
444
}
445

            
446
=head2 output_summary_text
447

            
448
Output summary in text format
449

            
450
=cut
451

            
452
sub output_summary_text {
453
    my ($fh, $summary) = @_;
454

            
455
    print $fh "SYSTEM OVERVIEW\n";
456
    print $fh "-"x40 . "\n";
457

            
458
    print $fh "Drive Status:\n";
459
    foreach my $status (sort keys %{$summary->{drive_counts}}) {
460
        printf $fh "  %-10s: %d drives\n", ucfirst($status), $summary->{drive_counts}->{$status};
461
    }
462

            
463
    if (%{$summary->{recent_predictions}}) {
464
        print $fh "\nRecent Risk Predictions:\n";
465
        foreach my $level (qw(critical high moderate low)) {
466
            next unless $summary->{recent_predictions}->{$level};
467
            printf $fh "  %-10s: %d drives\n", ucfirst($level), $summary->{recent_predictions}->{$level};
468
        }
469
    }
470

            
471
    if (%{$summary->{recent_alerts}}) {
472
        print $fh "\nRecent Alerts:\n";
473
        foreach my $type (sort keys %{$summary->{recent_alerts}}) {
474
            printf $fh "  %-15s: %d alerts\n", $type, $summary->{recent_alerts}->{$type};
475
        }
476
    }
477

            
478
    if ($summary->{collection_stats}) {
479
        my $stats = $summary->{collection_stats};
480
        print $fh "\nData Collection:\n";
481
        print $fh "  Total readings: $stats->{total_readings}\n";
482
        print $fh "  Devices monitored: $stats->{devices_with_data}\n";
483
        print $fh "  Success rate: $stats->{success_rate}%\n";
484
    }
485

            
486
    print $fh "\n";
487
}
488

            
489
=head2 output_json
490

            
491
Output report as JSON
492

            
493
=cut
494

            
495
sub output_json {
496
    my ($fh, $data) = @_;
497

            
498
    my $json = JSON::XS->new->pretty->encode($data);
499
    print $fh $json;
500
}
501

            
502
=head2 output_html
503

            
504
Output report as HTML (basic implementation)
505

            
506
=cut
507

            
508
sub output_html {
509
    my ($fh, $data, $type) = @_;
510

            
511
    print $fh <<'EOF';
512
<!DOCTYPE html>
513
<html>
514
<head>
515
    <title>autoSMART Report</title>
516
    <style>
517
        body { font-family: Arial, sans-serif; margin: 20px; }
518
        table { border-collapse: collapse; width: 100%; }
519
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
520
        th { background-color: #f2f2f2; }
521
        .critical { color: #d32f2f; font-weight: bold; }
522
        .high { color: #f57c00; font-weight: bold; }
523
        .moderate { color: #fbc02d; }
524
        .low { color: #388e3c; }
525
    </style>
526
</head>
527
<body>
528
EOF
529

            
530
    print $fh "<h1>autoSMART Report - " . ucfirst($type) . "</h1>\n";
531
    print $fh "<p>Generated: " . strftime("%Y-%m-%d %H:%M:%S", localtime($data->{generated_at})) . "</p>\n";
532

            
533
    # Basic HTML output - could be expanded significantly
534
    print $fh "<pre>" . encode_json($data) . "</pre>\n";
535

            
536
    print $fh "</body></html>\n";
537
}
538

            
539
# Additional text output functions would go here...
540
sub output_detailed_text {
541
    my ($fh, $drives) = @_;
542
    # Implementation for detailed drive output
543
    print $fh "DETAILED DRIVE INFORMATION\n";
544
    print $fh "-"x40 . "\n";
545
    foreach my $drive (@$drives) {
546
        print $fh "Device: $drive->{device_path}\n";
547
        print $fh "Model: " . ($drive->{model_name} || 'Unknown') . "\n";
548
        print $fh "Serial: " . ($drive->{serial_number} || 'Unknown') . "\n";
549
        print $fh "Status: $drive->{status}\n";
550
        if ($drive->{latest_prediction}) {
551
            print $fh "Latest Risk: $drive->{latest_prediction}->{risk_level}\n";
552
        }
553
        print $fh "\n";
554
    }
555
}
556

            
557
sub output_health_text {
558
    my ($fh, $health) = @_;
559
    print $fh "DRIVE HEALTH OVERVIEW\n";
560
    print $fh "-"x40 . "\n";
561
    foreach my $drive (@$health) {
562
        print $fh "$drive->{device_path}: $drive->{status}";
563
        print $fh " (Risk: $drive->{risk_level})" if $drive->{risk_level};
564
        print $fh "\n";
565
    }
566
}
567

            
568
sub output_alerts_text {
569
    my ($fh, $alerts) = @_;
570
    print $fh "ALERT HISTORY\n";
571
    print $fh "-"x40 . "\n";
572
    foreach my $alert (@$alerts) {
573
        printf $fh "%s [%s] %s: %s\n",
574
            $alert->{sent_at},
575
            $alert->{alert_type},
576
            $alert->{device_path},
577
            $alert->{message} || '';
578
    }
579
}
580

            
581
sub output_trends_text {
582
    my ($fh, $trends) = @_;
583
    print $fh "TREND ANALYSIS\n";
584
    print $fh "-"x40 . "\n";
585
    foreach my $device (sort keys %$trends) {
586
        print $fh "Device: $device\n";
587
        foreach my $trend (@{$trends->{$device}}) {
588
            print $fh "  $trend->{date}: Temp $trend->{avg_temp}°C ($trend->{reading_count} readings)\n";
589
        }
590
        print $fh "\n";
591
    }
592
}
593

            
594
=head2 print_help
595

            
596
Display help information
597

            
598
=cut
599

            
600
sub print_help {
601
    print <<'EOF';
602
autoSMART Report Generator v1.0
603

            
604
USAGE:
605
    autosmart-report.pl [OPTIONS]
606

            
607
OPTIONS:
608
    --config-dir DIR     Configuration directory (default: /etc/autosmart)
609
    --report TYPE        Report type (default: summary)
610
                         summary   - System overview and statistics
611
                         detailed  - Detailed drive information
612
                         health    - Current health status of all drives
613
                         alerts    - Alert history
614
                         trends    - Trend analysis
615
    --device PATH        Generate report for specific device only
616
    --days N            Days of history to include (default: 30)
617
    --format FORMAT     Output format: text, html, json (default: text)
618
    --output FILE       Write to file instead of stdout
619
    --help              Show this help message
620

            
621
EXAMPLES:
622
    # System summary
623
    autosmart-report.pl --report summary
624

            
625
    # Detailed report for specific drive
626
    autosmart-report.pl --report detailed --device /dev/sda
627

            
628
    # Health status as HTML
629
    autosmart-report.pl --report health --format html --output health.html
630

            
631
    # Alert history for last week
632
    autosmart-report.pl --report alerts --days 7
633

            
634
    # Trend analysis as JSON
635
    autosmart-report.pl --report trends --format json
636

            
637
REPORT TYPES:
638
    summary     High-level system statistics and overview
639
    detailed    Comprehensive information about each drive
640
    health      Current health status summary
641
    alerts      Recent alerts and notifications
642
    trends      Temperature and performance trends
643

            
644
OUTPUT FORMATS:
645
    text        Human-readable text format (default)
646
    html        HTML report with basic styling
647
    json        Machine-readable JSON format
648

            
649
EOF
650
}
651

            
652
__END__
653

            
654
=head1 AUTHOR
655

            
656
AutoSMART Development Team
657

            
658
=head1 LICENSE
659

            
660
This software is part of the autoSMART project.
661

            
662
=cut