Madagascar / projects / autoSMART / scripts / autosmart-predictor.pl
Newer Older
f16725e 3 months ago History
483 lines | 11.997kb
Bogdan Timofte authored 3 months ago
1
#!/usr/bin/perl
2

            
3
use strict;
4
use warnings;
5
use FindBin qw($Bin);
6
use lib "$Bin/../lib";
7

            
8
use PredictionEngine;
9
use Getopt::Long;
10
use JSON::XS;
11
use POSIX qw(strftime);
12

            
13
=head1 NAME
14

            
15
autosmart-predictor.pl - AI-powered HDD failure prediction for autoSMART
16

            
17
=head1 SYNOPSIS
18

            
19
    autosmart-predictor.pl [OPTIONS]
20

            
21
=head1 OPTIONS
22

            
23
    --config-dir DIR     Configuration directory (default: /etc/autosmart)
24
    --device PATH        Analyze specific device only
25
    --all               Analyze all active drives
26
    --days-back N       Days of history to analyze (default: 90)
27
    --output FORMAT     Output format: text, json, csv (default: text)
28
    --risk-level LEVEL  Show only drives with risk >= LEVEL (low, moderate, high, critical)
29
    --quiet             Quiet mode - only output results
30
    --debug             Enable debug logging
31
    --help              Show this help
32

            
33
=head1 DESCRIPTION
34

            
35
This script uses AI (OpenAI GPT) to analyze SMART data trends and predict
36
HDD failures. It processes historical SMART data stored by the collector
37
and generates intelligent predictions with confidence levels.
38

            
39
=cut
40

            
41
# Configuration
42
my $config_dir = '/etc/autosmart';
43
my $specific_device = '';
44
my $analyze_all = 0;
45
my $days_back = 90;
46
my $output_format = 'text';
47
my $min_risk_level = '';
48
my $quiet = 0;
49
my $debug = 0;
50
my $help = 0;
51

            
52
GetOptions(
53
    'config-dir=s'   => \$config_dir,
54
    'device=s'       => \$specific_device,
55
    'all'            => \$analyze_all,
56
    'days-back=i'    => \$days_back,
57
    'output=s'       => \$output_format,
58
    'risk-level=s'   => \$min_risk_level,
59
    'quiet'          => \$quiet,
60
    'debug'          => \$debug,
61
    'help'           => \$help,
62
) or die "Error parsing command line arguments\n";
63

            
64
if ($help) {
65
    print_help();
66
    exit 0;
67
}
68

            
69
# Validate options
70
unless ($specific_device || $analyze_all) {
71
    die "Must specify either --device PATH or --all\n";
72
}
73

            
74
if ($specific_device && $analyze_all) {
75
    die "Cannot specify both --device and --all\n";
76
}
77

            
78
unless ($output_format =~ /^(text|json|csv)$/) {
79
    die "Invalid output format: $output_format (must be text, json, or csv)\n";
80
}
81

            
82
if ($min_risk_level && $min_risk_level !~ /^(low|moderate|high|critical)$/) {
83
    die "Invalid risk level: $min_risk_level (must be low, moderate, high, or critical)\n";
84
}
85

            
86
# Validate configuration directory
87
unless (-d $config_dir) {
88
    die "Configuration directory not found: $config_dir\n";
89
}
90

            
91
my $db_config = "$config_dir/database.conf";
92
my $openai_config = "$config_dir/openai.conf";
93

            
94
unless (-f $db_config && -f $openai_config) {
95
    die "Required configuration files not found in $config_dir\n";
96
}
97

            
98
# Initialize prediction engine
99
my $predictor = PredictionEngine->new(
100
    db_config     => $db_config,
101
    openai_config => $openai_config,
102
    debug         => $debug,
103
);
104

            
105
log_message("autoSMART predictor starting...") unless $quiet;
106

            
107
my @predictions = ();
108

            
109
if ($specific_device) {
110
    # Analyze specific device
111
    log_message("Analyzing device: $specific_device") unless $quiet;
112

            
113
    my $prediction = $predictor->predict_failure($specific_device, $days_back);
114
    push @predictions, $prediction;
115

            
116
} elsif ($analyze_all) {
117
    # Analyze all active drives
118
    log_message("Analyzing all active drives...") unless $quiet;
119

            
120
    @predictions = @{$predictor->analyze_all_drives()};
121
}
122

            
123
# Filter predictions by minimum risk level if specified
124
if ($min_risk_level) {
125
    @predictions = filter_by_risk_level(\@predictions, $min_risk_level);
126
}
127

            
128
# Output results
129
output_predictions(\@predictions, $output_format);
130

            
131
log_message("Analysis complete") unless $quiet;
132

            
133
=head2 filter_by_risk_level
134

            
135
Filter predictions by minimum risk level
136

            
137
=cut
138

            
139
sub filter_by_risk_level {
140
    my ($predictions, $min_level) = @_;
141

            
142
    my %risk_order = (
143
        'low'      => 1,
144
        'moderate' => 2,
145
        'high'     => 3,
146
        'critical' => 4,
147
    );
148

            
149
    my $min_order = $risk_order{$min_level} || 1;
150

            
151
    return grep {
152
        exists $risk_order{$_->{risk_level}} &&
153
        $risk_order{$_->{risk_level}} >= $min_order
154
    } @$predictions;
155
}
156

            
157
=head2 output_predictions
158

            
159
Output predictions in specified format
160

            
161
=cut
162

            
163
sub output_predictions {
164
    my ($predictions, $format) = @_;
165

            
166
    if ($format eq 'json') {
167
        output_json($predictions);
168
    } elsif ($format eq 'csv') {
169
        output_csv($predictions);
170
    } else {
171
        output_text($predictions);
172
    }
173
}
174

            
175
=head2 output_json
176

            
177
Output predictions as JSON
178

            
179
=cut
180

            
181
sub output_json {
182
    my $predictions = shift;
183

            
184
    my $json = JSON::XS->new->pretty->encode({
185
        timestamp => time(),
186
        predictions => $predictions,
187
    });
188

            
189
    print $json;
190
}
191

            
192
=head2 output_csv
193

            
194
Output predictions as CSV
195

            
196
=cut
197

            
198
sub output_csv {
199
    my $predictions = shift;
200

            
201
    # CSV header
202
    print "device_path,timestamp,risk_level,confidence,time_to_failure_days,concerns,recommendations\n";
203

            
204
    foreach my $pred (@$predictions) {
205
        my @fields = (
206
            $pred->{device_path} || '',
207
            $pred->{timestamp} || '',
208
            $pred->{risk_level} || '',
209
            $pred->{confidence} || '',
210
            $pred->{time_to_failure_days} || '',
211
            escape_csv($pred->{concerns} || ''),
212
            escape_csv($pred->{recommendations} || ''),
213
        );
214

            
215
        print join(',', @fields) . "\n";
216
    }
217
}
218

            
219
=head2 output_text
220

            
221
Output predictions as human-readable text
222

            
223
=cut
224

            
225
sub output_text {
226
    my $predictions = shift;
227

            
228
    unless (@$predictions) {
229
        print "No predictions available.\n";
230
        return;
231
    }
232

            
233
    print "\n" . "="x80 . "\n";
234
    print "autoSMART HDD Failure Prediction Report\n";
235
    print "Generated: " . strftime("%Y-%m-%d %H:%M:%S", localtime()) . "\n";
236
    print "="x80 . "\n\n";
237

            
238
    foreach my $pred (@$predictions) {
239
        print_prediction_text($pred);
240
        print "-"x80 . "\n";
241
    }
242

            
243
    # Summary statistics
244
    my %risk_counts = ();
245
    my $total_confidence = 0;
246
    my $confidence_count = 0;
247

            
248
    foreach my $pred (@$predictions) {
249
        $risk_counts{$pred->{risk_level} || 'unknown'}++;
250

            
251
        if (defined $pred->{confidence} && $pred->{confidence} > 0) {
252
            $total_confidence += $pred->{confidence};
253
            $confidence_count++;
254
        }
255
    }
256

            
257
    print "\nSUMMARY:\n";
258
    print "Total drives analyzed: " . scalar(@$predictions) . "\n";
259

            
260
    foreach my $level (qw(critical high moderate low unknown)) {
261
        next unless $risk_counts{$level};
262
        print sprintf("%-10s risk: %d drives\n",
263
                     ucfirst($level), $risk_counts{$level});
264
    }
265

            
266
    if ($confidence_count > 0) {
267
        my $avg_confidence = $total_confidence / $confidence_count;
268
        print sprintf("Average confidence: %.1f%%\n", $avg_confidence);
269
    }
270

            
271
    print "\n";
272
}
273

            
274
=head2 print_prediction_text
275

            
276
Print a single prediction in text format
277

            
278
=cut
279

            
280
sub print_prediction_text {
281
    my $pred = shift;
282

            
283
    print "DEVICE: $pred->{device_path}\n";
284

            
285
    if ($pred->{prediction} eq 'insufficient_data') {
286
        print "STATUS: Insufficient data for analysis\n";
287
        print "MESSAGE: $pred->{message}\n";
288
        return;
289
    }
290

            
291
    print "RISK LEVEL: " . format_risk_level($pred->{risk_level}) . "\n";
292

            
293
    if (defined $pred->{confidence}) {
294
        print "CONFIDENCE: $pred->{confidence}%\n";
295
    }
296

            
297
    if (defined $pred->{time_to_failure_days} && $pred->{time_to_failure_days} > 0) {
298
        print "ESTIMATED TIME TO FAILURE: $pred->{time_to_failure_days} days\n";
299
    }
300

            
301
    if ($pred->{concerns}) {
302
        print "CONCERNS:\n";
303
        print format_text_block($pred->{concerns}, "  ");
304
    }
305

            
306
    if ($pred->{recommendations}) {
307
        print "RECOMMENDATIONS:\n";
308
        print format_text_block($pred->{recommendations}, "  ");
309
    }
310

            
311
    if ($pred->{reasoning} && $debug) {
312
        print "AI REASONING:\n";
313
        print format_text_block($pred->{reasoning}, "  ");
314
    }
315

            
316
    my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime($pred->{timestamp}));
317
    print "ANALYZED: $timestamp\n";
318

            
319
    print "\n";
320
}
321

            
322
=head2 format_risk_level
323

            
324
Format risk level with color coding (if terminal supports it)
325

            
326
=cut
327

            
328
sub format_risk_level {
329
    my $level = shift || 'unknown';
330

            
331
    # Simple color codes (won't work in all terminals)
332
    my %colors = (
333
        'critical' => "\033[1;31m",  # Bold red
334
        'high'     => "\033[0;31m",  # Red
335
        'moderate' => "\033[0;33m",  # Yellow
336
        'low'      => "\033[0;32m",  # Green
337
        'unknown'  => "\033[0;37m",  # Gray
338
    );
339

            
340
    my $reset = "\033[0m";
341

            
342
    # Only use colors if output is to terminal
343
    if (-t STDOUT) {
344
        return ($colors{$level} || '') . uc($level) . $reset;
345
    } else {
346
        return uc($level);
347
    }
348
}
349

            
350
=head2 format_text_block
351

            
352
Format multi-line text with indentation
353

            
354
=cut
355

            
356
sub format_text_block {
357
    my ($text, $indent) = @_;
358

            
359
    return '' unless $text;
360

            
361
    $indent ||= '';
362

            
363
    my @lines = split /\n/, $text;
364
    return join("\n", map { "$indent$_" } @lines) . "\n";
365
}
366

            
367
=head2 escape_csv
368

            
369
Escape CSV field content
370

            
371
=cut
372

            
373
sub escape_csv {
374
    my $field = shift || '';
375

            
376
    # Escape quotes and wrap in quotes if contains comma/quote/newline
377
    if ($field =~ /[",\n]/) {
378
        $field =~ s/"/""/g;
379
        $field = "\"$field\"";
380
    }
381

            
382
    return $field;
383
}
384

            
385
=head2 log_message
386

            
387
Log message with timestamp
388

            
389
=cut
390

            
391
sub log_message {
392
    my $message = shift;
393

            
394
    my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime());
395
    print STDERR "[$timestamp] autosmart-predictor: $message\n";
396
}
397

            
398
=head2 print_help
399

            
400
Display help information
401

            
402
=cut
403

            
404
sub print_help {
405
    print <<'EOF';
406
autoSMART AI Predictor v1.0
407

            
408
USAGE:
409
    autosmart-predictor.pl [OPTIONS]
410

            
411
OPTIONS:
412
    --config-dir DIR     Configuration directory (default: /etc/autosmart)
413
    --device PATH        Analyze specific device only (e.g., /dev/sda)
414
    --all               Analyze all active drives in inventory
415
    --days-back N       Days of SMART history to analyze (default: 90)
416
    --output FORMAT     Output format: text, json, csv (default: text)
417
    --risk-level LEVEL  Show only drives with risk >= LEVEL
418
                        (low, moderate, high, critical)
419
    --quiet             Quiet mode - suppress status messages
420
    --debug             Enable debug logging and show AI reasoning
421
    --help              Show this help message
422

            
423
EXAMPLES:
424
    # Analyze specific drive
425
    autosmart-predictor.pl --device /dev/sda
426

            
427
    # Analyze all drives
428
    autosmart-predictor.pl --all
429

            
430
    # Analyze with 30 days of history
431
    autosmart-predictor.pl --all --days-back 30
432

            
433
    # Show only high/critical risk drives
434
    autosmart-predictor.pl --all --risk-level high
435

            
436
    # Output as JSON
437
    autosmart-predictor.pl --all --output json
438

            
439
    # Quiet CSV output for scripts
440
    autosmart-predictor.pl --all --output csv --quiet
441

            
442
RISK LEVELS:
443
    LOW         No immediate concerns detected
444
    MODERATE    Some parameters showing minor degradation
445
    HIGH        Multiple concerning trends detected
446
    CRITICAL    Immediate action required - failure likely soon
447

            
448
OUTPUT FORMATS:
449
    text        Human-readable report (default)
450
    json        Machine-readable JSON format
451
    csv         Comma-separated values for spreadsheets
452

            
453
CONFIGURATION:
454
    Required configuration files in /etc/autosmart/:
455
    - database.conf     PostgreSQL connection settings
456
    - openai.conf      OpenAI API configuration
457

            
458
    The predictor requires historical SMART data collected by
459
    autosmart-collector.pl to generate meaningful predictions.
460

            
461
AI INTEGRATION:
462
    This tool uses OpenAI's GPT models to analyze SMART parameter trends
463
    and generate intelligent failure predictions. Ensure your OpenAI API
464
    key is properly configured in openai.conf.
465

            
466
EXIT CODES:
467
    0   Success
468
    1   Error (configuration, API failure, etc.)
469

            
470
EOF
471
}
472

            
473
__END__
474

            
475
=head1 AUTHOR
476

            
477
AutoSMART Development Team
478

            
479
=head1 LICENSE
480

            
481
This software is part of the autoSMART project.
482

            
483
=cut