Skip to content

Commit

Permalink
Console redirection
Browse files Browse the repository at this point in the history
  • Loading branch information
lpalovsky committed Mar 6, 2024
1 parent 86ff8ad commit 2bec08f
Show file tree
Hide file tree
Showing 7 changed files with 652 additions and 0 deletions.
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 serial_terminal qw(select_serial_terminal);
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
verify_ip
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.
=cut
sub handle_login_prompt {
my $pwd = get_var('_SECRET_SUT_PASSWORD', $testapi::password);
set_serial_term_prompt();
my $serial_response = wait_serial(qr/Password:\s*$|:~/i);
# Handle password prompt if it appears
if (grep /Password:\s*$/, $serial_response) {
type_password $pwd;
send_key 'ret';
}
croak 'Failed to setup serial console' unless grep /:~/, $serial_response;
}
=head2 redirection_init
redirection_init();
Do preparation before redirecting console. Gets base VM id and initial setup.
Needs to be done only once.
=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');
}
=head2 set_serial_term_prompt
set_serial_term_prompt();
Detects if login prompt appears and types the password.
=cut
sub set_serial_term_prompt {
$testapi::distri->{serial_term_prompt} = ''; # reset previous prompt first
my $current_user = script_output('whoami');
croak 'SSH user not defined and/or "whoami" script failed.' unless defined($current_user);
$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]);
B<ssh_user> Login user - can be alternatively defined by OpenQA parameter REDIRECT_TARGET_USER
B<target_ip> Target host IP - can be alternatively defined by OpenQA parameter REDIRECT_TARGET_IP
Detects if login prompt appears and types the password.
=cut
sub connect_target_to_serial {
my (%args) = @_;
my $redirect_ip = $args{'target_ip'} // get_required_var('REDIRECT_TARGET_IP');
my $ssh_user = $args{'ssh_user'} // get_required_var('REDIRECT_TARGET_USER');

croak 'Missing "ssh_user" argument' unless $ssh_user;
croak 'Missing "redirect_ip" argument' unless $redirect_ip;
croak "OpenQA variable WORKER_VM_ID undefined. Run 'redirection_init()' first" unless get_var('WORKER_VM_ID');
croak "IP address '$redirect_ip' is not valid." unless verify_ip($redirect_ip);
croak 'Global variable "$searialdev" undefined' unless $serialdev;
croak "Console is already redirected to:" . script_output('hostname') if check_serial_redirection();

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

=head2 disconnect_target_from_serial
disconnect_target_from_serial($worker_id);
Disconnects target from serial console by typing 'exit' command until host IP matches IP addr of the worker VM.
=cut
sub disconnect_target_from_serial {
my ($worker_id) = @_;
my $worker_machine_id = $worker_id // get_required_var('WORKER_VM_ID');
set_serial_term_prompt();
my $serial_redirection_status = check_serial_redirection($worker_machine_id);
while ($serial_redirection_status ne 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($worker_machine_id);
}
record_info('Redirect OFF', "Serial redirection closed. Console set to: " . script_output('hostname'));
}


=head2 verify_ip
verify_ip($ip_address);
B<ip_address> IP address
Validates IP address format. Returns IP address if format is valied, 0 if invalid.
=cut
sub verify_ip {
my ($ip_address) = @_;
my $result = grep(/^$RE{net}{IPv4}$/, $ip_address) ? $ip_address : 0;
return $result;
}

=head2 check_serial_redirection
check_serial_redirection([$source_machine_id]);
B<$source_machine_id> /etc/machine-id of the original machine where the console is redirected from
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 ($source_machine_id) = @_;
my $expected_id = $source_machine_id // get_required_var('WORKER_VM_ID');
my $current_id = script_output 'cat /etc/machine-id';

my $redirection_status = $current_id eq $expected_id ? 0 : 1;
record_info('Redir check', 'Console redirection is not active') unless $redirection_status;
record_info('Redir check', "Console is redirected to: " . script_output('hostname')) if $redirection_status;
return $redirection_status;
}

1;
177 changes: 177 additions & 0 deletions lib/sles4sap/microsoft_sdaf.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# SUSE's openQA tests
#
# Copyright SUSE LLC
# SPDX-License-Identifier: FSFAP
# Maintainer: QE-SAP <[email protected]>
#
# Library used for Microsoft SDAF deployment

package sles4sap::microsoft_sdaf;

use strict;
use warnings;
use testapi;
use Exporter qw(import);
use Carp qw(croak);
use mmapi qw(get_current_job_id);
use utils qw(write_sut_file file_content_replace);
use qesapdeployment qw(qesap_az_get_resource_group);
use File::Basename;
use Regexp::Common qw(net);

=head1 SYNOPSIS
Library with common functions for Microsoft SDAF deployment automation:
https://learn.microsoft.com/en-us/azure/sap/automation/get-started
=cut

our @EXPORT = qw(
az_login
sdaf_prepare_ssh_keys
sdaf_get_deployer_ip
);

=head2 az_login
az_login();
Azure login using SPN credentials defined by secret OpenQA parameters:
B<_SECRET_AZURE_SPN_APPLICATION_ID>
B<_SECRET_AZURE_SPN_APP_PASSWORD>
B<_SECRET_AZURE_ARM_TENANT_ID>
Returns 'subscription ID' on success.
=cut

sub az_login {
# Note: For login I cannot use standard PC library, because it gives credentials which are missing permissions
my $app_id = get_required_var('_SECRET_AZURE_SDAF_APP_ID');
my $app_secret = get_required_var('_SECRET_AZURE_SDAF_APP_PASSWORD');
my $tenant_id = get_required_var('_SECRET_AZURE_SDAF_TENANT_ID');
my @secret_variables = (
"export ARM_CLIENT_ID=$app_id",
"export ARM_CLIENT_SECRET=$app_secret",
"export ARM_TENANT_ID=$tenant_id"
);

write_bashrc_variables(@secret_variables);

my $login_cmd = 'while ! az login --service-principal -u ${ARM_CLIENT_ID} -p ${ARM_CLIENT_SECRET} -t ${ARM_TENANT_ID}; do sleep 10; done';
assert_script_run($login_cmd, timeout => 120);

my $subscription_id = script_output('az account show -o tsv --query id');
return ($subscription_id);
}

=head2 write_bashrc_variables
write_bashrc_variables(@variable list);
=cut

sub write_bashrc_variables {
my (@variables) = @_;

write_sut_file('/root/az_variables', join("\n", "\n", @variables));
assert_script_run('cat /root/az_variables >> /root/.bashrc', quiet => 1);
assert_script_run('source /root/.bashrc', quiet => 1);
}

=head2 az_get_ssh_key
az_get_ssh_key(deployer_key_vault=$deployer_key_vault, ssh_key_name=$key_name, ssh_key_filename=$ssh_key_filename);
B<deployer_key_vault> Deployer key vault name
B<ssh_key_name> SSH key name residing on keyvault
B<ssh_key_filename> Target filename for SSH key
Retrieves SSH key from DEPLOYER keyvault.
=cut

sub az_get_ssh_key {
my (%args) = @_;
my $cmd = join(' ',
'az', 'keyvault', 'secret', 'show',
'--vault-name', $args{deployer_key_vault},
'--name', $args{ssh_key_name},
'--query', 'value',
'--output', 'tsv', '>', "/root/.ssh/$args{ssh_key_filename}");

my $rc = 1;
my $retry = 3;
while ($rc) {
$rc = script_run($cmd);
last if $rc;
croak 'Failed to retrieve ssh key from keyvault' unless $retry;
$retry--;
sleep 5;
}
}

=head2 sdaf_prepare_ssh_keys
sdaf_prepare_ssh_keys(deployer_key_vault=$deployer_key_vault, ssh_key_name=$key_name);
B<deployer_key_vault> Deployer key vault name
B<ssh_key_name> SSH key name residing on keyvault
Retrieves public and private ssh key from DEPLOYER keyvault and sets up permissions.
=cut

sub sdaf_prepare_ssh_keys {
my (%args) = @_;
croak 'Missing mandatory argument $args{deployer_key_vault}' unless $args{deployer_key_vault};

my $az_cmd = "az keyvault secret list --vault-name $args{deployer_key_vault} --query [].name --output tsv";
my %ssh_keys = (
id_rsa => script_output("$az_cmd | grep sshkey\$"),
'id_rsa.pub' => script_output("$az_cmd | grep sshkey-pub\$")
);

assert_script_run('mkdir -p /root/.ssh');
assert_script_run('chmod 700 /root/.ssh');
for my $key_file (keys %ssh_keys) {
az_get_ssh_key(
deployer_key_vault => $args{deployer_key_vault},
ssh_key_name => $ssh_keys{$key_file},
ssh_key_filename => $key_file
);
}
assert_script_run("chmod 600 /root/.ssh/id_rsa");
assert_script_run("chmod 644 /root/.ssh/id_rsa.pub");
}

=head2 sdaf_get_deployer_ip
sdaf_get_deployer_ip(deployer_resource_group=>$deployer_resource_group);
B<deployer_resource_group> Deployer key vault name
Retrieves public IP of the deployer VM.
=cut

sub sdaf_get_deployer_ip {
my (%args) = @_;
croak 'Missing "deployer_resource_group" argument' unless $args{deployer_resource_group};

my $vm_name = script_output("az vm list --resource-group $args{deployer_resource_group} --query [].name --output tsv");
my $az_query_cmd = join(' ', 'az', 'vm', 'list-ip-addresses', '--resource-group', $args{deployer_resource_group},
'--name', $vm_name, '--query', '"[].virtualMachine.network.publicIpAddresses[0].ipAddress"', '-o', 'tsv');

my $ip_addr = script_output($az_query_cmd);
croak "Not a valid ip addr: $ip_addr" unless grep /^$RE{net}{IPv4}$/, $ip_addr;
return $ip_addr;
}

1;
28 changes: 28 additions & 0 deletions lib/sles4sap/microsoft_sdaf_basetest.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SUSE's openQA tests
#
# Copyright SUSE LLC
# SPDX-License-Identifier: FSFAP
# Maintainer: QE-SAP <[email protected]>
#
# Basetest used for Microsoft SDAF deployment

package sles4sap::microsoft_sdaf_basetest;
use strict;
use warnings;
use testapi;
use parent 'opensusebasetest';
use sles4sap::microsoft_sdaf;

sub post_fail_hook {
record_info('Post fail', 'Executing post fail hook');
}

sub post_run_hook {
record_info('Post run', 'Executing post run hook');
unless (get_var('SDAF_DO_CLEANUP')) {
record_info('Skip cleanup', "Openqa variable 'SDAF_DO_CLEANUP' not set, skipping cleanup");
return;
}
}

1;
7 changes: 7 additions & 0 deletions schedule/sles4sap/microsoft_sdaf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
name: microsoft_sdaf
description: |
microsoft sdaf based deployment
schedule:
- boot/boot_to_desktop
- sles4sap/microsoft_sdaf/sdaf_prepare_jumphost
Loading

0 comments on commit 2bec08f

Please sign in to comment.