Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Console redirection #18815

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions lib/sles4sap/console_redirection.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SUSE's openQA tests
#
# Copyright SUSE LLC
# SPDX-License-Identifier: FSFAP
# Maintainer: QE-SAP <[email protected]>

package sles4sap::console_redirection;
use strict;
use warnings;
use testapi;
use Carp qw(croak);
use Exporter qw(import);
use Regexp::Common qw(net); use Regexp::Common qw(net);

=head1 SYNOPSIS

Library that enables console redirection and file transfers from worker based VM to another host.
Can be used for cases where worker VM is not the target host for API calls and command execution, but serves only as a jumphost.

=cut

our @EXPORT = qw(
connect_target_to_serial
disconnect_target_from_serial
redirection_init
check_serial_redirection
);

my $ssh_opt = '-o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=120';

=head2 handle_login_prompt

handle_login_prompt();

B<ssh_user>: Login user

Detects if login prompt appears and types the password.
In case of ssh keys being in place and command prompt appears, the function does not type anything.

=cut

sub handle_login_prompt {
my $pwd = get_var('_SECRET_SUT_PASSWORD', $testapi::password);
set_serial_term_prompt();
# look for either password prompt or command prompt to appear
my $serial_response = wait_serial(qr/Password:\s*$|:~/, timeout => 20);

die 'Neither password not command prompt appeared.' unless $serial_response;
# Handle password prompt if it appears
if (grep /Password:\s*$/, $serial_response) {
type_password $pwd;
send_key 'ret';
# wait for command prompt to be ready
die 'Command prompt did not appear within timeout' unless wait_serial(qr/:~|#|>/, timeout => 20);
}
set_serial_term_prompt(); # set correct serial prompt
}

=head2 redirection_init

redirection_init();

Do preparation before redirecting console. Gets base VM id and initial setup.
This is required to be done only once at the beginning of the whole test.
If you have a multi machine setup, execute this on each worker VM.

=cut

sub redirection_init {
# This should get worker VM id before any redirection happening
# ID serves as identification of the 'base' VM to return to.
set_var('WORKER_VM_ID', script_output 'cat /etc/machine-id');
mpagot marked this conversation as resolved.
Show resolved Hide resolved
}

=head2 set_serial_term_prompt
mpagot marked this conversation as resolved.
Show resolved Hide resolved

set_serial_term_prompt();

Set expected serial prompt according to user which is currently active.
This changes global setting $testapi::distri->{serial_term_prompt} which is important for calls like wait_for_serial.

=cut

sub set_serial_term_prompt {
$testapi::distri->{serial_term_prompt} = ''; # reset previous prompt first
my $current_user = script_output('whoami');
$testapi::distri->{serial_term_prompt} = ($current_user eq 'root' ? '# ' : '> ');
}

=head2 connect_target_to_serial

connect_target_to_serial([ssh_user=>ssh_user, target_ip=>$target_ip]);
alvarocarvajald marked this conversation as resolved.
Show resolved Hide resolved

B<ssh_user>: Login user - default value is defined by OpenQA parameter REDIRECT_TARGET_USER
lpalovsky marked this conversation as resolved.
Show resolved Hide resolved

B<target_ip>: Target host IP - default value is defined by OpenQA parameter REDIRECT_TARGET_IP

Establishes ssh connection to target and redirects serial output to serial concole on worker VM.
This allows OpenQA access to command return codes and output for evaulation by standard API call.

=cut

sub connect_target_to_serial {
my (%args) = @_;
$args{'target_ip'} //= get_required_var('REDIRECT_TARGET_IP');
$args{'ssh_user'} //= get_required_var('REDIRECT_TARGET_USER');

croak "OpenQA variable WORKER_VM_ID undefined. Run 'redirection_init()' first" unless get_var('WORKER_VM_ID');
croak "IP address '$args{'target_ip'}' is not valid." unless grep(/^$RE{net}{IPv4}$/, $args{'target_ip'});
croak 'Global variable "$serialdev" undefined' unless $serialdev;
croak "Console is already redirected to:" . script_output('hostname') if check_serial_redirection();

enter_cmd "ssh $ssh_opt $args{'ssh_user'}\@$args{'target_ip'} 2>&1 | tee -a /dev/$serialdev";
handle_login_prompt($args{'ssh_user'});
check_serial_redirection();
record_info('Redirect ON', "Serial redirection established to: $args{'target_ip'}");
}

=head2 disconnect_target_from_serial

disconnect_target_from_serial([worker_machine_id=$worker_machine_id]);
alvarocarvajald marked this conversation as resolved.
Show resolved Hide resolved

B<worker_machine_id>: Target host IP - default value is defined by OpenQA parameter WORKER_VM_ID from redirect_init()

Disconnects target from serial console by typing 'exit' command until host machine ID matches ID of the worker VM.

lpalovsky marked this conversation as resolved.
Show resolved Hide resolved
=cut

sub disconnect_target_from_serial {
my (%args) = @_;
$args{worker_machine_id} //= get_required_var('WORKER_VM_ID');
set_serial_term_prompt();
my $serial_redirection_status = check_serial_redirection(worker_machine_id => $args{worker_machine_id});
lpalovsky marked this conversation as resolved.
Show resolved Hide resolved
while ($serial_redirection_status != 0) {
enter_cmd('exit'); # Enter command and wait for screen start changing
$testapi::distri->{serial_term_prompt} = ''; # reset console prompt
wait_serial(qr/Connection.*closed./, timeout => 10); # Wait for connection to close
wait_serial(qr/# |> /, timeout => 10); # Wait for console prompt to appear
set_serial_term_prompt(); # after logout user might change and prompt with it.
$serial_redirection_status = check_serial_redirection($args{worker_machine_id});
}
record_info('Redirect OFF', "Serial redirection closed. Console set to: " . script_output('hostname'));
}

=head2 check_serial_redirection

check_serial_redirection([worker_machine_id=$worker_machine_id]);
alvarocarvajald marked this conversation as resolved.
Show resolved Hide resolved

B<worker_machine_id>: Target host IP - default value is defined by OpenQA parameter WORKER_VM_ID from redirect_init()

Compares current machine-id to the worker VM ID either defined by WORKER_VM_ID variable or positional argument.
Machine ID is used instead of IP addr since cloud VM IP might not be visible from the inside (for example via 'ip a')

=cut

sub check_serial_redirection {
my (%args) = @_;
$args{worker_machine_id} //= get_required_var('WORKER_VM_ID');
my $current_id = script_output 'cat /etc/machine-id';
my $redirection_status = $current_id eq $args{worker_machine_id} ? 0 : 1;
my $logmsg = $redirection_status ? 'Console is redirected to: ' . script_output('hostname') : 'Console redirection is not active';

record_info('Redir check', $logmsg);
return $redirection_status;
}

1;
122 changes: 122 additions & 0 deletions t/19_console_redirection.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use strict;
use warnings;
use Test::Mock::Time;
use Test::More;
use Test::Exception;
use Test::Warnings;
use Test::MockModule;
use testapi;
use sles4sap::console_redirection;

our $serialdev = 'ttyS0'; # this is a global OpenQA variable

# make cleaning vars easier at the end of the unit test
sub unset_vars {
my @variables = ('REDIRECT_TARGET_IP', 'WORKER_VM_ID');
set_var($_, undef) foreach @variables;
}

subtest '[connect_target_to_serial] Expected failures' => sub {
my $redirect = Test::MockModule->new('sles4sap::console_redirection', no_auto => 1);
$redirect->redefine(enter_cmd => sub { return 1; });
$redirect->redefine(handle_login_prompt => sub { return 1; });
$redirect->redefine(record_info => sub { return 1; });
$redirect->redefine(script_output => sub { return 'castleinthesky'; });
set_var('WORKER_VM_ID', '7902847fcc554911993686a1d5eca2c8');
$redirect->redefine(check_serial_redirection => sub { return 0; });

dies_ok { connect_target_to_serial(target_ip => '192.168.1.1') } 'Fail with missing ssh user';
dies_ok { connect_target_to_serial(ssh_user => 'totoro') } 'Fail with missing ip address';
$redirect->redefine(check_serial_redirection => sub { return 1; });
dies_ok { connect_target_to_serial(ssh_user => 'totoro', target_ip => '192.168.1.1') } 'Fail with console already being redirected';
unset_vars();
dies_ok { connect_target_to_serial(ssh_user => ' ', target_ip => '192.168.1.1') } 'Fail with user defined as empty space';
};

subtest '[connect_target_to_serial] Test passing behavior' => sub {
my $redirect = Test::MockModule->new('sles4sap::console_redirection', no_auto => 1);
my $ssh_cmd;
$redirect->redefine(enter_cmd => sub { $ssh_cmd = $_[0]; return 1; });
$redirect->redefine(handle_login_prompt => sub { return 1; });
$redirect->redefine(record_info => sub { return 1; });
$redirect->redefine(check_serial_redirection => sub { return 0; });
$redirect->redefine(script_output => sub { return 'castleinthesky'; });
set_var('WORKER_VM_ID', '7902847fcc554911993686a1d5eca2c8');
connect_target_to_serial(target_ip => '192.168.1.1', ssh_user => 'totoro');
is $ssh_cmd, 'ssh -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=120 [email protected] 2>&1 | tee -a /dev/ttyS0', 'Pass with corect command executed';
unset_vars();
};

subtest '[handle_login_prompt] Test via "connect_to_serial"' => sub {
my $redirect = Test::MockModule->new('sles4sap::console_redirection', no_auto => 1);
my $type_pass_executed = 0;
$redirect->redefine(enter_cmd => sub { return 1; });
$redirect->redefine(type_password => sub { $type_pass_executed = 1; });
$redirect->redefine(send_key => sub { return 1; });
$redirect->redefine(set_serial_term_prompt => sub { return 1; });
$redirect->redefine(record_info => sub { return 1; });
$redirect->redefine(check_serial_redirection => sub { return 0; });
$redirect->redefine(script_output => sub { return 'castleinthesky'; });
set_var('WORKER_VM_ID', '7902847fcc554911993686a1d5eca2c8');

my @command_prompts = ('laputa@castleinthesky:~>', 'castleinthesky:~ # ');
foreach (@command_prompts) {
$redirect->redefine(wait_serial => sub { return $_; });
connect_target_to_serial(target_ip => '192.168.1.1', ssh_user => 'totoro');
is $type_pass_executed, 0, "Pass with command prompt detected: $_";
$type_pass_executed = 0; # reset flag
}

$redirect->redefine(wait_serial => sub { return '(laputa@castleinthesky) Password:'; });
$redirect->redefine(croak => sub { return; }); # Need to disable croak since wait_serial won't return second response here.

connect_target_to_serial(target_ip => '192.168.1.1', ssh_user => 'totoro');
is $type_pass_executed, 1, 'Pass with password prompt detected';
unset_vars();
};

subtest '[disconnect_target_from_serial]' => sub {
my $redirect = Test::MockModule->new('sles4sap::console_redirection', no_auto => 1);
my $wait_serial_done = 0; # Flag that code entered while loop
$redirect->redefine(record_info => sub { return 1; });
$redirect->redefine(wait_serial => sub { $wait_serial_done = 1; return ':~'; });
$redirect->redefine(enter_cmd => sub { return 1; });
$redirect->redefine(check_serial_redirection => sub { return $wait_serial_done; });
$redirect->redefine(set_serial_term_prompt => sub { return 1; });
$redirect->redefine(script_output => sub { return ''; });

ok disconnect_target_from_serial(worker_machine_id => '7902847fcc554911993686a1d5eca2c8'), 'Pass with machine ID defined by positional argument';

set_var('WORKER_VM_ID', '7902847fcc554911993686a1d5eca2c8');
ok disconnect_target_from_serial(), 'Pass with machine ID defined by parameter WORKER_VM_ID';
unset_vars();

dies_ok { disconnect_target_from_serial() } 'Fail without specifying machine ID and WORKER_VM_ID undefined';
};

subtest '[redirection_init]' => sub {
my $redirect = Test::MockModule->new('sles4sap::console_redirection', no_auto => 1);
$redirect->redefine(script_output => sub { return '7902847fcc554911993686a1d5eca2c8'; });

redirection_init();
is get_var('WORKER_VM_ID'), '7902847fcc554911993686a1d5eca2c8', 'Pass with WORKER_VM_ID being set correctly';
unset_vars();
};

subtest '[check_serial_redirection]' => sub {
my $redirect = Test::MockModule->new('sles4sap::console_redirection', no_auto => 1);
$redirect->redefine(script_output => sub { return '7902847fcc554911993686a1d5eca2c8'; });
$redirect->redefine(record_info => sub { return; });

set_var('WORKER_VM_ID', '7902847fcc554911993686a1d5eca2c8');
is check_serial_redirection(), '0', 'Return 0 if machine IDs match';
set_var('WORKER_VM_ID', '999999999999999999999999');
is check_serial_redirection(), '1', 'Return 1 if machine IDs do not match';

unset_vars();

is check_serial_redirection(worker_machine_id => '123456'), '1', 'Pass with specifying ID via positional argument';
dies_ok { check_serial_redirection() } 'Fail with WORKER_VM_ID being unset';
};

done_testing;
Loading