-
Notifications
You must be signed in to change notification settings - Fork 283
/
Copy pathqesapdeployment.pm
2622 lines (2008 loc) · 83.4 KB
/
qesapdeployment.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SUSE's openQA tests
#
# Copyright 2022 SUSE LLC
# SPDX-License-Identifier: FSFAP
#
# Summary: Functions to use qe-sap-deployment project
# Maintainer: QE-SAP <[email protected]>
## no critic (RequireFilenameMatchesPackage);
=encoding utf8
=head1 NAME
qe-sap-deployment test lib
=head1 COPYRIGHT
Copyright 2022 SUSE LLC
SPDX-License-Identifier: FSFAP
=head1 AUTHORS
QE SAP <[email protected]>
=cut
package qesapdeployment;
use strict;
use warnings;
use Carp qw(croak);
use Mojo::JSON qw(decode_json);
use YAML::PP;
use utils qw(file_content_replace);
use version_utils 'is_sle';
use publiccloud::utils qw(get_credentials);
use sles4sap::azure_cli;
use mmapi 'get_current_job_id';
use testapi;
use Exporter 'import';
use Scalar::Util 'looks_like_number';
use File::Basename;
my @log_files = ();
# Terraform requirement that constrain QESAPDEPLOY_PREFIX value
# terraform/azure/infrastructure.tf "azurerm_storage_account" "mytfstorageacc"
# stdiag<PREFID><JOB_ID> can only consist of lowercase letters and numbers,
# and must be between 3 and 24 characters long
use constant QESAPDEPLOY_PREFIX => 'qesapdep';
use constant QESAPDEPLOY_VENV => '/tmp/exec_venv';
use constant QESAPDEPLOY_PY_DEFAULT_VER => '3.11';
our @EXPORT = qw(
qesap_upload_logs
qesap_get_deployment_code
qesap_get_roles_code
qesap_get_inventory
qesap_get_nodes_number
qesap_get_nodes_names
qesap_get_terraform_dir
qesap_get_ansible_roles_dir
qesap_prepare_env
qesap_execute
qesap_execute_conditional_retry
qesap_ansible_cmd
qesap_ansible_script_output_file
qesap_ansible_script_output
qesap_ansible_fetch_file
qesap_ansible_reg_module
qesap_create_ansible_section
qesap_remote_hana_public_ips
qesap_wait_for_ssh
qesap_cluster_log_cmds
qesap_cluster_logs
qesap_upload_crm_report
qesap_aws_get_region_subnets
qesap_aws_get_vpc_id
qesap_aws_create_transit_gateway_vpc_attachment
qesap_aws_delete_transit_gateway_vpc_attachment
qesap_aws_get_transit_gateway_vpc_attachment
qesap_aws_add_route_to_tgw
qesap_aws_get_mirror_tg
qesap_aws_get_vpc_workspace
qesap_aws_get_routing
qesap_aws_vnet_peering
qesap_add_server_to_hosts
qesap_calculate_deployment_name
qesap_export_instances
qesap_import_instances
qesap_file_find_string
qesap_is_job_finished
qesap_calculate_address_range
qesap_az_get_resource_group
qesap_az_vnet_peering
qesap_az_simple_peering_delete
qesap_az_vnet_peering_delete
qesap_az_get_active_peerings
qesap_az_clean_old_peerings
qesap_az_setup_native_fencing_permissions
qesap_az_get_tenant_id
qesap_az_create_sas_token
qesap_az_list_container_files
qesap_az_diagnostic_log
qesap_terrafom_ansible_deploy_retry
qesap_test_postfail
);
=head1 DESCRIPTION
Package with common methods and default or constant values for qe-sap-deployment
=head2 Methods
=head3 qesap_get_file_paths
Returns a hash containing file paths for configuration files
=cut
sub qesap_get_file_paths {
my %paths;
$paths{qesap_conf_filename} = get_required_var('QESAP_CONFIG_FILE');
$paths{deployment_dir} = get_var('QESAP_DEPLOYMENT_DIR', '/root/qe-sap-deployment');
$paths{terraform_dir} = get_var('PUBLIC_CLOUD_TERRAFORM_DIR', $paths{deployment_dir} . '/terraform');
$paths{qesap_conf_trgt} = $paths{deployment_dir} . '/scripts/qesap/' . $paths{qesap_conf_filename};
$paths{qesap_conf_src} = data_url('sles4sap/qe_sap_deployment/' . $paths{qesap_conf_filename});
$paths{roles_dir} = get_var('QESAP_ROLES_DIR', '/root/community.sles-for-sap');
$paths{roles_dir_path} = $paths{roles_dir} . '/roles';
return (%paths);
}
=head3 qesap_create_folder_tree
Create all needed folders
=cut
sub qesap_create_folder_tree {
my %paths = qesap_get_file_paths();
die 'Missing deployment_dir in paths' unless $paths{deployment_dir};
assert_script_run("mkdir -p $paths{deployment_dir}", quiet => 1);
die 'Missing roles_dir in paths' unless $paths{roles_dir};
assert_script_run("mkdir -p $paths{roles_dir}", quiet => 1);
}
=head3 qesap_get_variables
Scans yaml configuration for '%OPENQA_VARIABLE%' placeholders and
searches for values in OpenQA defined variables.
Returns hash with openqa variable key/value pairs.
=cut
sub qesap_get_variables {
my %paths = qesap_get_file_paths();
die "Missing mandatory qesap_conf_src from qesap_get_file_paths()" unless $paths{'qesap_conf_src'};
my $yaml_file = $paths{'qesap_conf_src'};
my %variables;
my $cmd = join(' ',
'curl -s -fL', $yaml_file, '|',
'grep -v', "'#'", '|',
'grep -oE %[A-Z0-9_]*%', '|',
'sed s/%//g');
for my $variable (split(" ", script_output($cmd))) {
$variables{$variable} = get_required_var($variable);
}
return \%variables;
}
=head3 qesap_create_ansible_section
Writes "ansible" section into yaml configuration file.
$args{ansible_section} defines section(key) name.
$args{section_content} defines content of names section.
Example:
@playbook_list = ("pre-cluster.yaml", "cluster_sbd_prep.yaml");
qesap_create_ansible_section(ansible_section=>'create', section_content=>\@playbook_list);
=cut
sub qesap_create_ansible_section {
my (%args) = @_;
my $ypp = YAML::PP->new;
my $section = $args{ansible_section} // 'no_section_provided';
my $content = $args{section_content} // {};
my %paths = qesap_get_file_paths();
my $yaml_config_path = $paths{qesap_conf_trgt};
assert_script_run("test -e $yaml_config_path",
fail_message => "Yaml config file '$yaml_config_path' does not exist.");
my $raw_file = script_output("cat $yaml_config_path");
my $yaml_data = $ypp->load_string($raw_file);
$yaml_data->{ansible}{$section} = $content;
# write into file
my $yaml_dumped = $ypp->dump_string($yaml_data);
save_tmp_file($paths{qesap_conf_filename}, $yaml_dumped);
my $cmd = join(' ', 'curl', '-v',
'-fL', autoinst_url . "/files/" . $paths{qesap_conf_filename},
'-o', $paths{qesap_conf_trgt});
assert_script_run($cmd);
return;
}
=head3 qesap_venv_cmd_exec
Run a command within the Python virtualenv
created by qesap_pip_install.
This function never dies: it always returns an error to the caller.
Timeout error is 124 (the one reported by timeout command line utility).
=over
=item B<CMD> - command to run within the .venv, usually it is a qesap.py based command
=item B<TIMEOUT> - default 90 secs, has to be an integer greater than 0
=item B<LOG_FILE> - optional argument that results in changing the command to redirect the output to a log file
=back
=cut
sub qesap_venv_cmd_exec {
my (%args) = @_;
croak 'Missing mandatory cmd argument' unless $args{cmd};
$args{timeout} //= bmwqemu::scale_timeout(90);
croak "Invalid timeout value $args{timeout}" unless $args{timeout} > 0;
my $cmd = '';
# pipefail is needed as at the end of the command line there could be a pipe
# to redirect all output to a log_file.
# pipefail allow script_run always getting the exit code of the cmd command
# and not only the one from tee
$cmd .= 'set -o pipefail ; ' if $args{log_file};
$cmd .= join(' ', 'timeout', $args{timeout}, $args{cmd});
# always use tee in append mode
$cmd .= " |& tee -a $args{log_file}" if $args{log_file};
my $ret = script_run('source ' . QESAPDEPLOY_VENV . '/bin/activate');
if ($ret) {
record_info('qesap_venv_cmd_exec error', "source .venv ret:$ret");
return $ret;
}
$ret = script_run($cmd, timeout => ($args{timeout} + 10));
# deactivate python virtual environment
script_run('deactivate');
return $ret;
}
=head3 qesap_py
Return string of the python to use
=cut
sub qesap_py {
return 'python' . get_var('QESAP_PYTHON_VERSION', QESAPDEPLOY_PY_DEFAULT_VER);
}
=head3 qesap_pip
Return string of the pip to use
=cut
sub qesap_pip {
return 'pip' . get_var('QESAP_PIP_VERSION', QESAPDEPLOY_PY_DEFAULT_VER);
}
=head3 qesap_pip_install
Install all Python requirements of the qe-sap-deployment
in a dedicated virtual environment.
This function has no return code but it is expected to die
if something internally fails.
=cut
sub qesap_pip_install {
my %paths = qesap_get_file_paths();
my $pip_install_log = '/tmp/pip_install.txt';
my $pip_ints_cmd = join(' ', qesap_pip(), 'install --no-color --no-cache-dir',
'-r', "$paths{deployment_dir}/requirements.txt");
# Create a Python virtualenv
assert_script_run(join(' ', qesap_py(), '-m venv', QESAPDEPLOY_VENV));
# Configure pip in it, ignore the return value
# and does not provide any timeout as it should be
# instantaneous
qesap_venv_cmd_exec(cmd => qesap_pip() . ' config --site set global.progress_bar off');
push(@log_files, $pip_install_log);
record_info('QESAP repo', 'Installing all qe-sap-deployment python requirements');
my $ret = qesap_venv_cmd_exec(
cmd => $pip_ints_cmd,
timeout => 720,
log_file => $pip_install_log);
# here it is possible to retry in case of exit code 124
die "cmd:$pip_ints_cmd --> ret:$ret" if $ret;
}
=head3 qesap_galaxy_install
Install all Ansible requirements of the qe-sap-deployment.
This function has no return code but it is expected to die
if something internally fails.
=cut
sub qesap_galaxy_install {
my %paths = qesap_get_file_paths();
my $galaxy_install_log = '/tmp/galaxy_install.txt';
my $ans_req = "$paths{deployment_dir}/requirements.yml";
my $ans_galaxy_cmd = join(' ',
'ansible-galaxy install',
'-r', $ans_req);
push(@log_files, $galaxy_install_log);
my $ret = qesap_venv_cmd_exec(
cmd => $ans_galaxy_cmd,
timeout => 720,
log_file => $galaxy_install_log);
# here it is possible to retry in case of exit code 124
die "cmd:$ans_galaxy_cmd --> ret:$ret" if $ret;
}
=head3 qesap_upload_logs
qesap_upload_logs([failok=1])
Collect and upload logs present in @log_files.
=over
=item B<FAILOK> - used as failok for the upload_logs. continue even in case upload fails
=back
=cut
sub qesap_upload_logs {
my (%args) = @_;
$args{failok} //= 0;
record_info("Uploading logfiles failok:$args{failok}", join("\n", @log_files));
while (my $file = pop @log_files) {
upload_logs($file, failok => $args{failok});
}
}
=head3 qesap_get_deployment_code
Get the qe-sap-deployment code
=cut
sub qesap_get_deployment_code {
my $official_repo = 'github.com/SUSE/qe-sap-deployment';
my $qesap_git_clone_log = '/tmp/git_clone.txt';
my %paths = qesap_get_file_paths();
die "Missing mandatory terraform_dir from qesap_get_file_paths()" unless $paths{'terraform_dir'};
record_info('QESAP repo', 'Preparing qe-sap-deployment repository');
enter_cmd("cd " . $paths{deployment_dir});
push(@log_files, $qesap_git_clone_log);
# Script from a release
if (get_var('QESAP_INSTALL_VERSION')) {
record_info('WARNING', 'QESAP_INSTALL_GITHUB_REPO will be ignored') if (get_var('QESAP_INSTALL_GITHUB_REPO'));
record_info('WARNING', 'QESAP_INSTALL_GITHUB_BRANCH will be ignored') if (get_var('QESAP_INSTALL_GITHUB_BRANCH'));
my $ver_artifact;
if (check_var('QESAP_INSTALL_VERSION', 'latest')) {
my $latest_release_url = "https://$official_repo/releases/latest";
my $redirect_url = script_output("curl -s -L -o /dev/null -w %{url_effective} $latest_release_url");
die "Failed to parse the latest version from $redirect_url" if ($redirect_url !~ /\/tag\/v([0-9.]+)$/);
my $version = $1;
$ver_artifact = "v$version.tar.gz";
record_info("Vesion latest", "Latest QE-SAP-DEPLOYMENT release used: $version");
}
else {
$ver_artifact = 'v' . get_var('QESAP_INSTALL_VERSION') . '.tar.gz';
}
my $curl_cmd = "curl -v -fL https://$official_repo/archive/refs/tags/$ver_artifact -o$ver_artifact";
assert_script_run("set -o pipefail ; $curl_cmd | tee " . $qesap_git_clone_log, quiet => 1);
my $tar_cmd = "tar xvf $ver_artifact --strip-components=1";
assert_script_run($tar_cmd);
}
else {
# Get the code for the qe-sap-deployment by cloning its repository
assert_script_run('git config --global http.sslVerify false', quiet => 1) if get_var('QESAP_INSTALL_GITHUB_NO_VERIFY');
my $git_branch = get_var('QESAP_INSTALL_GITHUB_BRANCH', 'main');
my $git_repo = get_var('QESAP_INSTALL_GITHUB_REPO', $official_repo);
my $git_clone_cmd = 'git clone --depth 1 --branch ' . $git_branch . ' https://' . $git_repo . ' ' . $paths{deployment_dir};
assert_script_run("set -o pipefail ; $git_clone_cmd |& tee $qesap_git_clone_log", quiet => 1);
}
# Add symlinks for different provider directory naming between OpenQA and qesap-deployment
assert_script_run("ln -s " . $paths{terraform_dir} . "/aws " . $paths{terraform_dir} . "/ec2");
assert_script_run("ln -s " . $paths{terraform_dir} . "/gcp " . $paths{terraform_dir} . "/gce");
}
=head3 qesap_get_roles_code
Get the Ansible roles code from github.com/sap-linuxlab/community.sles-for-sap
Keep in mind that to allow qe-sap-deployment to use roles from this repo,
your config.yaml has to have a specific setting ansible::roles_path.
=cut
sub qesap_get_roles_code {
my $official_repo = 'github.com/sap-linuxlab/community.sles-for-sap';
my $roles_git_clone_log = '/tmp/git_clone_roles.txt';
my %paths = qesap_get_file_paths();
record_info('SLES4SAP Roles repo', 'Preparing community.sles-for-sap repository');
enter_cmd("cd " . $paths{roles_dir});
push(@log_files, $roles_git_clone_log);
# Script from a release
if (get_var('QESAP_ROLES_INSTALL_VERSION')) {
die('community.sles-for-sap does not implement releases yet.');
}
else {
# Get the code for the community.sles-for-sap by cloning its repository
assert_script_run('git config --global http.sslVerify false', quiet => 1) if get_var('QESAP_INSTALL_GITHUB_NO_VERIFY');
my $git_branch = get_var('QESAP_ROLES_INSTALL_GITHUB_BRANCH', 'main');
my $git_repo = get_var('QESAP_ROLES_INSTALL_GITHUB_REPO', $official_repo);
my $git_clone_cmd = join(' ', 'git clone',
'--depth 1',
"--branch $git_branch",
'https://' . $git_repo,
$paths{roles_dir});
assert_script_run("set -o pipefail ; $git_clone_cmd |& tee $roles_git_clone_log", quiet => 1);
}
}
=head3 qesap_yaml_replace
Replaces yaml configuration file variables with parameters
defined by OpenQA test code, yaml template or yaml schedule.
Openqa variables need to be added as a hash
with key/value pair inside %run_args{openqa_variables}.
Example:
my %variables;
$variables{HANA_SAR} = get_required_var("HANA_SAR");
$variables{HANA_CLIENT_SAR} = get_required_var("HANA_CLIENT_SAR");
qesap_yaml_replace(openqa_variables=>\%variables);
=cut
sub qesap_yaml_replace {
my (%args) = @_;
my $variables = $args{openqa_variables};
my %replaced_variables = ();
my %paths = qesap_get_file_paths();
push(@log_files, $paths{qesap_conf_trgt});
for my $variable (keys %{$variables}) {
$replaced_variables{"%" . $variable . "%"} = $variables->{$variable};
}
file_content_replace($paths{qesap_conf_trgt}, %replaced_variables);
qesap_upload_logs();
}
=head3 qesap_execute
qesap_execute(
cmd => 'terraform',
logname => 'terraform_destroy.log.txt'
[, verbose => 1, cmd_options => $cmd_options] );
Example:
qesap_execute(cmd => 'terraform', cmd_options => '-d')
result in:
qesap.py terraform -d
Execute qesap glue script commands. Check project documentation for available options:
https://github.com/SUSE/qe-sap-deployment
Function returns a two element array:
- first element is an integer representing the execution result
- second element is the file path of the execution log
This function is not expected to internally die, any failure has to be handled by the caller.
=over
=item B<CMD> - qesap.py subcommand to run
=item B<LOGNAME> - filename of the log file. This file will be saved in `/tmp` folder
=item B<CMD_OPTIONS> - set of arguments for the qesap.py subcommand
=item B<VERBOSE> - activate verbosity in qesap.py
=item B<TIMEOUT> - max expected execution time, default 90sec
=back
=cut
sub qesap_execute {
my (%args) = @_;
foreach (qw(cmd logname)) { croak "Missing mandatory $_ argument" unless $args{$_}; }
my $verbose = $args{verbose} ? "--verbose" : "";
$args{cmd_options} //= '';
$args{timeout} //= bmwqemu::scale_timeout(90);
my %paths = qesap_get_file_paths();
my $exec_log = '/tmp/' . $args{logname};
my $qesap_cmd = join(' ', qesap_py(), $paths{deployment_dir} . '/scripts/qesap/qesap.py',
$verbose,
'-c', $paths{qesap_conf_trgt},
'-b', $paths{deployment_dir},
$args{cmd},
$args{cmd_options});
push(@log_files, $exec_log);
record_info("QESAP exec $args{cmd}", "Executing: \n$qesap_cmd \n\nlog to $exec_log");
my $exec_rc = qesap_venv_cmd_exec(
cmd => $qesap_cmd,
timeout => $args{timeout},
log_file => $exec_log);
my @qesap_logs;
# look for logs produced directly by the qesap.py
my $qesap_log_find = 'find . -type f -name "*.log.txt"';
foreach my $log (split(/\n/, script_output($qesap_log_find, proceed_on_failure => 1))) {
push(@log_files, $log);
# Also record them in a dedicated list
# to be able to delete them as soon as they are uploaded.
# It is needed to not create duplicated uploads
# from different deployment stages (terraform, ansible, destroy, retry, ...)
push(@qesap_logs, $log);
}
qesap_upload_logs();
foreach (@qesap_logs) {
enter_cmd("rm -f $_");
}
my @results = ($exec_rc, $exec_log);
return @results;
}
=head3 qesap_execute_conditional_retry
qesap_execute_conditional_retry(
cmd => $qesap_script_cmd,
error_string => 'Fatal:',
logname => 'somefile.txt'
[, verbose => 1s] );
Execute qesap glue script commands. Check project documentation for available options:
https://github.com/SUSE/qe-sap-deployment
Test only returns execution result, failure has to be handled by calling method.
=over
=item B<CMD> - qesap.py subcommand to run
=item B<VERBOSE> - activate verbosity in qesap.py
=item B<TIMEOUT> - max expected execution time, default 90sec
=item B<LOGNAME> - filename of the log file.
=item B<RETRIES> - number of retries in case of expected error
=item B<ERROR_STRING> - error string to look for
=item B<destroy_terraform> - destroy terraform before retrying terraform apply
=back
=cut
sub qesap_execute_conditional_retry {
my (%args) = @_;
foreach (qw(cmd logname error_string)) { croak "Missing mandatory $_ argument" unless $args{$_}; }
my $verbose = $args{verbose} ? "--verbose" : "";
$args{timeout} //= bmwqemu::scale_timeout(90);
$args{retries} //= 1;
my @ret = qesap_execute(cmd => $args{cmd},
verbose => $args{verbose},
timeout => $args{timeout},
logname => $args{logname});
while ($args{retries} > 0) {
if ($ret[0]) {
if (qesap_file_find_string(file => $ret[1], search_string => $args{error_string})) {
record_info('DETECTED ' . uc($args{cmd}) . ' ERROR', $args{error_string});
# Executing terraform destroy before retrying terraform apply
if ($args{destroy_terraform}) {
qesap_execute(
cmd => 'terraform',
cmd_options => '-d',
logname => "qesap_exec_terraform_destroy_before_retry$args{retries}.log.txt",
verbose => 1,
timeout => 1200);
}
@ret = qesap_execute(cmd => $args{cmd},
logname => 'qesap_' . $args{cmd} . '_retry_' . $args{retries} . '.log.txt',
timeout => $args{timeout});
if ($ret[0] == 0) {
record_info('QESAP_EXECUTE RETRY PASS');
last;
}
} else {
die "'qesap.py $args{cmd}' return: $ret[0]";
}
} else {
last;
}
$args{retries}--;
}
if ($ret[0]) {
die "'qesap.py (after retry) $args{cmd}' return: $ret[0]";
}
# Sleep $N for fixing ansible "Missing sudo password" issue on GCP
if (get_required_var('PUBLIC_CLOUD_PROVIDER') eq 'GCE') {
sleep 60;
record_info('Workaround: "sleep 60" for fixing ansible "Missing sudo password" issue on GCP');
}
return @ret;
}
=head3 qesap_file_find_string
Search for a string in the Ansible log file.
Returns 1 if the string is found in the log file, 0 otherwise.
=over
=item B<FILE> - Path to the Ansible log file. (Required)
=item B<SEARCH_STRING> - String to search for in the log file. (Required)
=back
=cut
sub qesap_file_find_string {
my (%args) = @_;
foreach (qw(file search_string)) { croak "Missing mandatory $_ argument" unless $args{$_}; }
my $ret = script_run("grep \"$args{search_string}\" $args{file}");
return $ret == 0 ? 1 : 0;
}
=head3 qesap_get_inventory
Return the path of the generated inventory
=over
=item B<PROVIDER> - Cloud provider name using same format of PUBLIC_CLOUD_PROVIDER setting
=back
=cut
sub qesap_get_inventory {
my (%args) = @_;
croak "Missing mandatory argument 'provider'" unless $args{provider};
my %paths = qesap_get_file_paths();
return join('/', qesap_get_terraform_dir(provider => $args{provider}), 'inventory.yaml');
}
=head3 qesap_get_nodes_number
Get the number of cluster nodes from the inventory.yaml
=over
=item B<PROVIDER> - Cloud provider name using same format of PUBLIC_CLOUD_PROVIDER setting
=back
=cut
sub qesap_get_nodes_number {
my (%args) = @_;
croak "Missing mandatory argument 'provider'" unless $args{provider};
my $inventory = qesap_get_inventory(provider => $args{provider});
my $yp = YAML::PP->new();
my $inventory_content = script_output("cat $inventory");
my $parsed_inventory = $yp->load_string($inventory_content);
my $num_hosts = 0;
while ((my $key, my $value) = each(%{$parsed_inventory->{all}->{children}})) {
$num_hosts += keys %{$value->{hosts}};
}
return $num_hosts;
}
=head3 qesap_get_nodes_names
Get the cluster nodes' names from the inventory.yaml
=over
=item B<PROVIDER> - Cloud provider name using same format of PUBLIC_CLOUD_PROVIDER setting
=back
=cut
sub qesap_get_nodes_names {
my (%args) = @_;
croak "Missing mandatory argument 'provider'" unless $args{provider};
my $inventory = qesap_get_inventory(provider => $args{provider});
my $yp = YAML::PP->new();
my $inventory_content = script_output("cat $inventory");
my $parsed_inventory = $yp->load_string($inventory_content);
my @hosts;
while ((my $key, my $value) = each(%{$parsed_inventory->{all}->{children}})) {
if (exists $value->{hosts}) {
push @hosts, keys %{$value->{hosts}};
}
}
return @hosts;
}
=head3 qesap_get_terraform_dir
Return the path used by the qesap script as -chdir argument for terraform
It is useful if test would like to call terraform
=over
=item B<PROVIDER> - Cloud provider name using same format of PUBLIC_CLOUD_PROVIDER setting
=back
=cut
sub qesap_get_terraform_dir {
my (%args) = @_;
croak "Missing mandatory argument 'provider'" unless $args{provider};
my %paths = qesap_get_file_paths();
return join('/', $paths{terraform_dir}, lc $args{provider});
}
=head3 qesap_get_ansible_roles_dir
Return the path where sap-linuxlab/community.sles-for-sap
has been installed
=cut
sub qesap_get_ansible_roles_dir {
my %paths = qesap_get_file_paths();
return $paths{roles_dir_path};
}
=head3 qesap_prepare_env
qesap_prepare_env(variables=>{dict with variables}, provider => 'aws');
Prepare terraform environment.
- creates file structures
- pulls git repository
- external configuration files
- installs pip requirements and OS packages
- generates configuration files with qesap script
For variables example see 'qesap_yaml_replace'
Returns only result, failure handling has to be done by calling method.
=over
=item B<PROVIDER> - Cloud provider name, used to optionally activate AWS credential code
=back
=cut
sub qesap_prepare_env {
my (%args) = @_;
croak "Missing mandatory argument 'provider'" unless $args{provider};
my $variables = $args{openqa_variables} ? $args{openqa_variables} : qesap_get_variables();
my $provider_folder = lc $args{provider};
my %paths = qesap_get_file_paths();
die "Missing mandatory deployment_dir from qesap_get_file_paths()" unless $paths{'deployment_dir'};
die "Missing mandatory qesap_conf_trgt from qesap_get_file_paths()" unless $paths{'qesap_conf_trgt'};
# Option to skip straight to configuration
unless ($args{only_configure}) {
die "Missing mandatory qesap_conf_src from qesap_get_file_paths()" unless $paths{'qesap_conf_src'};
qesap_create_folder_tree();
qesap_get_deployment_code();
qesap_get_roles_code();
qesap_pip_install();
# for the moment run it only conditionally
# to allow this test code also to work with older
# qe-sap-deployment versions
qesap_galaxy_install() if (script_run("test -e $paths{deployment_dir}/requirements.yml") == 0);
record_info('QESAP yaml', 'Preparing yaml config file');
assert_script_run('curl -v -fL ' . $paths{qesap_conf_src} . ' -o ' . $paths{qesap_conf_trgt});
}
qesap_yaml_replace(openqa_variables => $variables);
push(@log_files, $paths{qesap_conf_trgt});
record_info('QESAP conf', 'Generating all terraform and Ansible configuration files');
my $terraform_tfvars = join('/',
qesap_get_terraform_dir(provider => $args{provider}),
'terraform.tfvars');
push(@log_files, $terraform_tfvars);
my $hana_media = "$paths{deployment_dir}/ansible/playbooks/vars/hana_media.yaml";
my $hana_vars = "$paths{deployment_dir}/ansible/playbooks/vars/hana_vars.yaml";
my @exec_rc = qesap_execute(cmd => 'configure', logname => 'qesap_configure.log.txt', verbose => 1);
if ($args{provider} eq 'EC2') {
my $data = get_credentials('aws.json');
qesap_create_aws_config();
qesap_create_aws_credentials($data->{access_key_id}, $data->{secret_access_key});
}
push(@log_files, $hana_media) if (script_run("test -e $hana_media") == 0);
push(@log_files, $hana_vars) if (script_run("test -e $hana_vars") == 0);
qesap_upload_logs(failok => 1);
die("Qesap deployment returned non zero value during 'configure' phase.") if $exec_rc[0];
return;
}
=head3 qesap_ansible_get_playbook
Download the playbook from the test code repo
that is on the worker within the running JompHost.
=cut
sub qesap_ansible_get_playbook {
my (%args) = @_;
croak 'Missing mandatory playbook argument' unless $args{playbook};
if (script_run("test -e $args{playbook}")) {
my $cmd = join(' ',
'curl', '-v', '-fL',
data_url("sles4sap/$args{playbook}"),
'-o', $args{playbook});
assert_script_run($cmd);
}
}
=head3 qesap_ansible_cmd
Use Ansible to run a command remotely on some or all
the hosts from the inventory.yaml
=over
=item B<PROVIDER> - Cloud provider name, used to find the inventory
=item B<CMD> - command to run remotely
=item B<USER> - user on remote host, default to 'cloudadmin'
=item B<FILTER> - filter hosts in the inventory
=item B<FAILOK> - if not set, Ansible failure result in die
=item B<HOST_KEYS_CHECK> - if set, add some extra argument to the Ansible call
to allow contacting hosts not in the KnownHost list yet.
This enables the use of this api before the call to qesap.py ansible
=item B<TIMEOUT> - default 90 secs
=item B<VERBOSE> - enable verbosity, default is OFF
=back
=cut
sub qesap_ansible_cmd {
my (%args) = @_;
foreach (qw(provider cmd)) { croak "Missing mandatory $_ argument" unless $args{$_}; }
$args{user} //= 'cloudadmin';
$args{filter} //= 'all';
$args{timeout} //= bmwqemu::scale_timeout(90);
$args{failok} //= 0;
my $verbose = $args{verbose} ? ' -vvvv' : '';
my $inventory = qesap_get_inventory(provider => $args{provider});
record_info('Ansible cmd:', "Run on '$args{filter}' node\ncmd: '$args{cmd}'");
my $ansible_cmd = join(' ',
'ansible' . $verbose,
$args{filter},
'-i', $inventory,
'-u', $args{user},
'-b', '--become-user=root',
'-a', "\"$args{cmd}\"");
$ansible_cmd = $args{host_keys_check} ?
join(' ', $ansible_cmd,
'-e',
"'ansible_ssh_common_args=\"-o UpdateHostKeys=yes -o StrictHostKeyChecking=accept-new\"'") :
$ansible_cmd;
my $ret = qesap_venv_cmd_exec(cmd => $ansible_cmd, timeout => $args{timeout});
die "cmd: $ansible_cmd ret: $ret" if ($ret && !$args{failok});
}
=head3 qesap_ansible_script_output_file
Use Ansible to run a command remotely and get the stdout.
Command could be executed with elevated privileges
qesap_ansible_script_output_file(cmd => 'crm status', provider => 'aws', host => 'vmhana01', root => 1);
It uses playbook data/sles4sap/script_output.yaml
1. ansible-playbook runs the playbook
2. the playbook executes the command remotely and redirects the output to file, both remotely
3. qesap_ansible_fetch_file downloads the file locally
4. the file is read and stored to be returned to the caller
Return is the local full path of the file containing the output of the
remotely executed command.
=over
=item B<PROVIDER> - Cloud provider name, used to find the inventory
=item B<CMD> - command to run remotely
=item B<HOST> - filter hosts in the inventory
=item B<FILE> - result file name
=item B<OUT_PATH> - path to save result file locally (without file name)
=item B<USER> - user on remote host, default to 'cloudadmin'
=item B<ROOT> - 1 to enable remote execution with elevated user, default to 0
=item B<FAILOK> - if not set, Ansible failure result in die
=item B<VERBOSE> - 1 result in ansible-playbook to be called with '-vvvv', default is 0.
=item B<TIMEOUT> - max expected execution time, default 180sec.
Same timeout is used both for the execution of script_output.yaml and for the fetch_file.
Timeout of the same amount is started two times.
=item B<REMOTE_PATH> - Path to save file in the remote (without file name)
=back
=cut
sub qesap_ansible_script_output_file {
my (%args) = @_;
foreach (qw(provider cmd host)) { croak "Missing mandatory $_ argument" unless $args{$_}; }
$args{user} //= 'cloudadmin';
$args{root} //= 0;
$args{failok} //= 0;
$args{timeout} //= bmwqemu::scale_timeout(180);
$args{verbose} //= 0;
my $verbose = $args{verbose} ? '-vvvv' : '';
$args{remote_path} //= '/tmp/';
$args{out_path} //= '/tmp/ansible_script_output/';
$args{file} //= 'testout.txt';
my $inventory = qesap_get_inventory(provider => $args{provider});
my $playbook = 'script_output.yaml';
qesap_ansible_get_playbook(playbook => $playbook);
my @ansible_cmd = ('ansible-playbook', $verbose, $playbook);
push @ansible_cmd, ('-l', $args{host}, '-i', $inventory, '-u', $args{user});
push @ansible_cmd, ('-b', '--become-user', 'root') if ($args{root});
push @ansible_cmd, ('-e', qq("cmd='$args{cmd}'"),
'-e', "out_file='$args{file}'", '-e', "remote_path='$args{remote_path}'");
push @ansible_cmd, ('-e', "failok=yes") if ($args{failok});
my $ret = qesap_venv_cmd_exec(cmd => join(' ', @ansible_cmd),
timeout => $args{timeout});
die "ret: $ret" if ($ret && !$args{failok});
# Grab the file from the remote
return qesap_ansible_fetch_file(provider => $args{provider},
host => $args{host},
failok => $args{failok},
user => $args{user},
root => $args{root},
remote_path => $args{remote_path},
out_path => $args{out_path},