Skip to content

Commit b6aa545

Browse files
committed
Refactor Azure diagnostic log retrieval and improve error handling
Centralize the logic for fetching Azure VM boot diagnostics in azure_cli.pm by enhancing az_vm_diagnostic_log_get with timeout support and better error reporting. Refactor qesap_az_diagnostic_log to use this centralized function, reducing code duplication. Update unit tests in 15_qesap_azure.t and 21_sles4sap_azure_cli.t to reflect these changes and cover new failure scenarios.
1 parent 59b179d commit b6aa545

File tree

4 files changed

+133
-116
lines changed

4 files changed

+133
-116
lines changed

lib/sles4sap/azure_cli.pm

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,19 +1427,36 @@ Return a list of diagnostic file paths on the JumpHost
14271427
14281428
=item B<resource_group> - existing resource group where to search for a specific VM
14291429
1430+
=item B<timeout> - max expected execution time for a single az command - default 240sec
1431+
14301432
=back
14311433
=cut
14321434

14331435
sub az_vm_diagnostic_log_get(%args) {
14341436
croak("Argument < resource_group > missing") unless $args{resource_group};
14351437

1438+
my $timeout = $args{timeout} // 240;
14361439
my @diagnostic_log_files;
14371440
my $vm_data = az_vm_list(resource_group => $args{resource_group}, query => '[].{id:id,name:name}');
14381441
my $az_get_logs_cmd = 'az vm boot-diagnostics get-boot-log --ids';
1442+
my $ret;
1443+
my $err;
1444+
my $boot_diagnostics_log;
14391445
foreach (@{$vm_data}) {
1440-
my $boot_diagnostics_log = '/tmp/boot-diagnostics_' . $_->{name} . '.txt';
1441-
push @diagnostic_log_files, $boot_diagnostics_log unless
1442-
script_run(join(' ', $az_get_logs_cmd, $_->{id}, '|&', 'tee', $boot_diagnostics_log));
1446+
$boot_diagnostics_log = '/tmp/boot-diagnostics_' . $_->{name} . '.txt';
1447+
eval {
1448+
$ret = script_run(
1449+
join(' ', $az_get_logs_cmd, $_->{id}, '|&', 'tee', $boot_diagnostics_log),
1450+
timeout => $timeout);
1451+
};
1452+
$err = $@;
1453+
if ($err || !defined $ret || $ret != 0) {
1454+
my $detail = ($err && $err =~ /\S/) ? "$err" : "Exit code: $ret";
1455+
my $fail_msg = "Failed to get boot diagnostics for $_->{name}: $detail";
1456+
record_info('Diag Warn', $fail_msg, result => 'fail');
1457+
next;
1458+
}
1459+
push(@diagnostic_log_files, $boot_diagnostics_log);
14431460
}
14441461
return @diagnostic_log_files;
14451462
}

lib/sles4sap/qesap/azure.pm

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ use Carp qw(croak);
3333
use Mojo::JSON qw(decode_json);
3434
use Exporter 'import';
3535
use File::Basename;
36+
use testapi;
37+
use mmapi 'get_current_job_id';
3638
use sles4sap::azure_cli;
3739
use sles4sap::qesap::utils;
38-
use mmapi 'get_current_job_id';
39-
use testapi;
4040

4141
our @EXPORT = qw(
4242
qesap_az_get_resource_group
@@ -325,33 +325,11 @@ Return a list of diagnostic file paths on the JumpHost
325325
sub qesap_az_diagnostic_log {
326326
my (%args) = @_;
327327
my $fatal = $args{fatal} // 1;
328-
my $timeout = $args{timeout} // 240;
329-
my @diagnostic_log_files;
330328
my @failures;
331329

332-
my $rg = qesap_az_get_resource_group();
333-
my $vm_data = az_vm_list(resource_group => $rg, query => '[].{id:id,name:name}');
334-
my $az_get_logs_cmd = 'az vm boot-diagnostics get-boot-log --ids';
335-
336-
foreach (@{$vm_data}) {
337-
record_info('az vm boot-diagnostics json', "id: $_->{id} name: $_->{name}");
338-
my $boot_diagnostics_log = '/tmp/boot-diagnostics_' . $_->{name} . '.txt';
339-
340-
my $ret;
341-
eval {
342-
$ret = script_run("$az_get_logs_cmd $_->{id} &> $boot_diagnostics_log", timeout => $timeout);
343-
};
344-
my $error = $@;
345-
346-
if ($error || !defined $ret || $ret != 0) {
347-
my $detail = ($error && $error =~ /\S/) ? "$error" : "Exit code: $ret";
348-
my $fail_msg = "Failed to get boot diagnostics for $_->{name}: $detail";
349-
record_info('Diag Warn', $fail_msg, result => 'fail');
350-
push @failures, $fail_msg;
351-
next;
352-
}
353-
push(@diagnostic_log_files, $boot_diagnostics_log);
354-
}
330+
my @diagnostic_log_files = az_vm_diagnostic_log_get(
331+
resource_group => qesap_az_get_resource_group(),
332+
timeout => $args{timeout} // 240);
355333

356334
if (@failures && $fatal) {
357335
die "Fatal Error:\n" . join("\n", @failures);

t/15_qesap_azure.t

Lines changed: 55 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -325,46 +325,72 @@ subtest '[qesap_az_diagnostic_log] no VMs' => sub {
325325
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
326326
my @calls;
327327
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
328+
$qesap->redefine(az_vm_diagnostic_log_get => sub { push @calls, {@_}; return (); });
329+
$qesap->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
330+
331+
my @log_files = qesap_az_diagnostic_log();
332+
333+
ok((any { $_->{resource_group} eq 'DENTIST' } @calls), 'Proper resource group in vm list');
334+
#ok((any { $_->{query} =~ /id:id,name:name/ } @calls), 'Proper query in vm list');
335+
ok((scalar @log_files == 0), 'No returned logs');
336+
};
337+
338+
subtest '[qesap_az_diagnostic_log] no VMs integration test' => sub {
339+
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
340+
my $azcli = Test::MockModule->new('sles4sap::azure_cli', no_auto => 1);
341+
my @calls;
342+
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
328343
$qesap->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
329344

330345
# Configure vm list to return no VMs
331-
$qesap->redefine(az_vm_list => sub { push @calls, {@_}; return []; });
346+
$azcli->redefine(script_output => sub { push @calls, $_[0]; return '[]'; });
332347

333348
my @log_files = qesap_az_diagnostic_log();
334349

335350
note("\n C--> " . join("\n C--> ", @calls));
336-
ok((any { $_->{resource_group} eq 'DENTIST' } @calls), 'Proper resource group in vm list');
337-
ok((any { $_->{query} =~ /id:id,name:name/ } @calls), 'Proper query in vm list');
351+
ok((any { /az vm list.*DENTIST.*/ } @calls), 'List all VMs in the resource group');
338352
ok((scalar @log_files == 0), 'No returned logs');
339353
};
340354

341355
subtest '[qesap_az_diagnostic_log] one VMs' => sub {
342356
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
343357
my @calls;
344358
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
359+
$qesap->redefine(az_vm_diagnostic_log_get => sub { push @calls, {@_}; return ('pink_coral.log'); });
345360
$qesap->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
346361

362+
my @log_files = qesap_az_diagnostic_log();
363+
364+
ok((scalar @log_files == 1), 'Exactly one returned logs for one VM');
365+
};
366+
367+
subtest '[qesap_az_diagnostic_log] one VMs integration test' => sub {
368+
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
369+
my $azcli = Test::MockModule->new('sles4sap::azure_cli', no_auto => 1);
370+
my @calls;
371+
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
372+
373+
$azcli->redefine(script_run => sub { push @calls, $_[0]; return 0; });
347374
# Configure vm list to return one VM
348-
$qesap->redefine(az_vm_list => sub {
349-
push @calls, {@_};
350-
return [{name => "NEMO", id => "MARLIN"}];
375+
$azcli->redefine(script_output => sub {
376+
push @calls, $_[0];
377+
return '[{"name":"NEMO", "id":"MARLIN"}]';
351378
});
352-
$qesap->redefine(script_run => sub { push @calls, $_[0]; return 0; });
379+
$azcli->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
353380

354381
my @log_files = qesap_az_diagnostic_log();
355382

356383
note("\n C--> " . join("\n C--> ", @calls));
357-
ok((any { $_->{resource_group} eq 'DENTIST' } @calls), 'Proper resource group in vm list');
358-
ok((any { /az vm boot-diagnostics get-boot-log.*/ } @calls), 'Proper base command for vm boot-diagnostics get-boot-log');
359-
ok((any { /.*--ids MARLIN.*/ } @calls), 'Proper id in boot-diagnostics');
360-
ok((any { /.*boot-diagnostics_NEMO.*/ } @calls), 'Proper output file in boot-diagnostics');
361384
ok((scalar @log_files == 1), 'Exactly one returned logs for one VM');
362385
};
363386

364387
subtest '[qesap_az_diagnostic_log] three VMs' => sub {
365388
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
366389
my @calls;
367390
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
391+
$qesap->redefine(az_vm_diagnostic_log_get => sub {
392+
push @calls, {@_};
393+
return ('pink_coral.log', 'blue_coral.log', 'white_coral.log'); });
368394
$qesap->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
369395

370396
# Configure vm list to return three VMs
@@ -378,10 +404,27 @@ subtest '[qesap_az_diagnostic_log] three VMs' => sub {
378404
});
379405
$qesap->redefine(script_run => sub { push @calls, $_[0]; return 0; });
380406

407+
my @log_files = qesap_az_diagnostic_log();
408+
ok((scalar @log_files == 3), 'Exactly three returned logs for three VMs');
409+
};
410+
411+
subtest '[qesap_az_diagnostic_log] three VMs integration test' => sub {
412+
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
413+
my $azcli = Test::MockModule->new('sles4sap::azure_cli', no_auto => 1);
414+
my @calls;
415+
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
416+
417+
$azcli->redefine(script_run => sub { push @calls, $_[0]; return 0; });
418+
# Configure vm list to return 3 VMs
419+
$azcli->redefine(script_output => sub {
420+
push @calls, $_[0];
421+
return '[{"name":"DORY", "id":"BLUE_TANG"},{"name":"BRUCE", "id":"GREAT_WHITE"},{"name":"CRUSH", "id":"SEA_TURTLE"}]';
422+
});
423+
$azcli->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
424+
381425
my @log_files = qesap_az_diagnostic_log();
382426

383427
note("\n C--> " . join("\n C--> ", @calls));
384-
ok((any { ref($_) eq 'HASH' && $_->{resource_group} eq 'DENTIST' } @calls), 'Proper resource group in vm list');
385428

386429
my %expected_vms = (
387430
DORY => "BLUE_TANG",
@@ -396,76 +439,4 @@ subtest '[qesap_az_diagnostic_log] three VMs' => sub {
396439
ok((scalar @log_files == 3), 'Exactly three returned logs for three VMs');
397440
};
398441

399-
subtest '[qesap_az_diagnostic_log] Partial Success (fatal = 0)' => sub {
400-
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
401-
my @record_calls;
402-
403-
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
404-
$qesap->redefine(az_vm_list => sub {
405-
return [
406-
{name => "DORY", id => "BLUE_TANG"},
407-
{name => "BRUCE", id => "GREAT_WHITE"},
408-
{name => "CRUSH", id => "SEA_TURTLE"}
409-
];
410-
});
411-
412-
$qesap->redefine(script_run => sub {
413-
my ($cmd) = @_;
414-
return ($cmd =~ /BRUCE/) ? 1 : 0;
415-
});
416-
$qesap->redefine(record_info => sub { push @record_calls, $_[0] if $_[0] eq 'Diag Warn'; });
417-
418-
my @log_files;
419-
lives_ok {
420-
@log_files = qesap_az_diagnostic_log(fatal => 0);
421-
} 'Fail silently';
422-
423-
is(scalar @log_files, 2, 'Return exactly 2 successful logs');
424-
ok((any { /boot-diagnostics_DORY.txt/ } @log_files), 'DORY log present');
425-
ok((any { /boot-diagnostics_CRUSH.txt/ } @log_files), 'CRUSH log present');
426-
ok(!(any { /boot-diagnostics_BRUCE.txt/ } @log_files), 'BRUCE log absent');
427-
is(scalar @record_calls, 1, 'One failure for BRUCE');
428-
};
429-
430-
subtest '[qesap_az_diagnostic_log] Cumulative Failures (fatal = 1)' => sub {
431-
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
432-
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
433-
$qesap->redefine(record_info => sub { 1; });
434-
435-
$qesap->redefine(az_vm_list => sub {
436-
return [
437-
{name => "NEMO", id => "MARLIN"},
438-
{name => "DORY", id => "BLUE_TANG"}
439-
];
440-
});
441-
442-
$qesap->redefine(script_run => sub { return 1; });
443-
throws_ok {
444-
qesap_az_diagnostic_log(fatal => 1);
445-
} qr/Fatal Error:.*NEMO.*DORY/s, 'Die as expected';
446-
};
447-
448-
subtest '[qesap_az_diagnostic_log] Mixed Error' => sub {
449-
my $qesap = Test::MockModule->new('sles4sap::qesap::azure', no_auto => 1);
450-
$qesap->redefine(qesap_az_get_resource_group => sub { return 'DENTIST'; });
451-
$qesap->redefine(record_info => sub { 1; });
452-
453-
$qesap->redefine(az_vm_list => sub {
454-
return [
455-
{name => "NEMO", id => "MARLIN"},
456-
{name => "DORY", id => "BLUE_TANG"}
457-
];
458-
});
459-
460-
$qesap->redefine(script_run => sub {
461-
my ($cmd) = @_;
462-
die "Timeout issue" if $cmd =~ /DORY/;
463-
return 1; # NEMO
464-
});
465-
466-
throws_ok {
467-
qesap_az_diagnostic_log(fatal => 1);
468-
} qr/Exit code: 1.*DORY: Timeout issue/s, 'Exitcode and timeout as expected';
469-
};
470-
471442
done_testing;

t/21_sles4sap_azure_cli.t

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -860,14 +860,65 @@ subtest '[az_vm_diagnostic_log_get]' => sub {
860860
push @calls, $_[0];
861861
# simulate 2 VM
862862
return '[{"id": "0001", "name": "Truffaldino"}, {"id": "0002", "name": "Mirandolina"}]'; });
863+
$azcli->redefine(script_run => sub { push @calls, $_[0]; return 0; });
864+
$azcli->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
865+
866+
my @ret = az_vm_diagnostic_log_get(resource_group => 'Arlecchino');
867+
868+
note("\n C--> " . join("\n C--> ", @calls));
869+
note("\n R--> " . join("\n R--> ", @ret));
870+
ok((any { /az vm list/ } @calls), 'Correct composition of the list command');
871+
ok((any { /az vm boot-diagnostics get-boot-log/ } @calls), 'Correct composition of the main command');
872+
ok((any { /--ids 0001.*tee.*Truffaldino\.txt/ } @calls), 'Correct composition of the --id for the first VM');
873+
ok((any { /--ids 0002.*tee.*Mirandolina\.txt/ } @calls), 'Correct composition of the --id for the second VM');
874+
875+
ok scalar @ret == 2, "Two logs one for each VM";
876+
ok((any { /\/tmp\/boot-diagnostics_.*/ } @ret), 'Correct log filenames');
877+
};
878+
879+
subtest '[az_vm_diagnostic_log_get] no VMs' => sub {
880+
my $azcli = Test::MockModule->new('sles4sap::azure_cli', no_auto => 1);
881+
my @calls;
882+
$azcli->redefine(script_output => sub {
883+
push @calls, $_[0];
884+
# simulate 2 VM
885+
return '[]'; });
863886
$azcli->redefine(script_run => sub { push @calls, $_[0]; });
887+
$azcli->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
864888

865-
az_vm_diagnostic_log_get(resource_group => 'Arlecchino');
889+
my @ret = az_vm_diagnostic_log_get(resource_group => 'Arlecchino');
866890

867-
note("\n --> " . join("\n --> ", @calls));
891+
note("\n C--> " . join("\n C--> ", @calls));
892+
note("\n R--> " . join("\n R--> ", @ret));
893+
ok((any { /az vm list/ } @calls), 'Correct composition of the list command');
894+
ok((none { /az vm boot-diagnostics get-boot-log/ } @calls), 'Correct composition of the main command');
895+
ok scalar @ret == 0, "No logs";
896+
};
897+
898+
subtest '[az_vm_diagnostic_log_get] one fail' => sub {
899+
my $azcli = Test::MockModule->new('sles4sap::azure_cli', no_auto => 1);
900+
my @calls;
901+
$azcli->redefine(script_output => sub {
902+
push @calls, $_[0];
903+
# simulate 2 VM
904+
return '[{"id": "0001", "name": "Truffaldino"}, {"id": "0002", "name": "Mirandolina"}]'; });
905+
$azcli->redefine(script_run => sub {
906+
my ($cmd) = @_;
907+
push @calls, $cmd;
908+
return ($cmd =~ /Mirandolina/) ? 1 : 0; });
909+
$azcli->redefine(record_info => sub { note(join(' ', 'RECORD_INFO -->', @_)); });
910+
911+
my @ret = az_vm_diagnostic_log_get(resource_group => 'Arlecchino');
912+
913+
note("\n C--> " . join("\n C--> ", @calls));
914+
note("\n R--> " . join("\n R--> ", @ret));
915+
ok((any { /az vm list/ } @calls), 'Correct composition of the list command');
868916
ok((any { /az vm boot-diagnostics get-boot-log/ } @calls), 'Correct composition of the main command');
869917
ok((any { /--ids 0001.*tee.*Truffaldino\.txt/ } @calls), 'Correct composition of the --id for the first VM');
870918
ok((any { /--ids 0002.*tee.*Mirandolina\.txt/ } @calls), 'Correct composition of the --id for the second VM');
919+
920+
ok scalar @ret == 1, "One log for the non failing VM";
921+
ok((any { /\/tmp\/boot-diagnostics_.*/ } @ret), 'Correct log filenames');
871922
};
872923

873924
subtest '[az_storage_account_create]' => sub {

0 commit comments

Comments
 (0)