2017-01-18 07:15:44 +00:00
package anaconda ;
use strict ;
use base 'Exporter' ;
use Exporter ;
use testapi ;
use utils ;
2021-04-27 11:02:53 +00:00
use bugzilla ;
2017-01-18 07:15:44 +00:00
2021-10-06 04:27:59 +00:00
our @ EXPORT = qw/select_disks custom_scheme_select custom_add_partition custom_blivet_add_partition custom_blivet_format_partition custom_blivet_resize_partition custom_change_type custom_change_fs custom_change_device custom_delete_part get_full_repo check_help_on_pane get_mirrorlist_url crash_anaconda_text report_bug_text/ ;
2017-01-18 07:15:44 +00:00
sub select_disks {
# Handles disk selection. Has one optional argument - number of
# disks to select. Should be run when main Anaconda hub is
# displayed. Enters disk selection spoke and then ensures that
# required number of disks are selected. Additionally, if
# PARTITIONING variable starts with custom_, selects "custom
# partitioning" checkbox. Example usage:
# after calling `select_disks(2);` from Anaconda main hub,
# installation destination spoke will be displayed and two
# attached disks will be selected for installation.
my % args = (
disks = > 1 ,
iscsi = > { } ,
@ _
) ;
my % iscsi = % { $ args { iscsi } } ;
# Anaconda hub
assert_screen "anaconda_main_hub" , 300 ; #
# Damn animation delay can cause bad clicks here too - wait for it
sleep 1 ;
assert_and_click "anaconda_main_hub_install_destination" ;
if ( get_var ( 'NUMDISKS' ) > 1 ) {
2017-08-28 07:55:04 +00:00
# Multi-disk case. Select however many disks the test needs. If
# $disks is 0, this will do nothing, and 0 disks will be selected.
for my $ n ( 1 .. $ args { disks } ) {
assert_and_click "anaconda_install_destination_select_disk_$n" ;
2017-01-18 07:15:44 +00:00
}
}
else {
2020-02-26 22:30:08 +00:00
# Single disk case.
2017-01-18 07:15:44 +00:00
if ( $ args { disks } == 0 ) {
2020-02-26 22:30:08 +00:00
# Clicking will *de*-select.
assert_and_click "anaconda_install_destination_select_disk_1" ;
2017-01-18 07:15:44 +00:00
}
elsif ( $ args { disks } > 1 ) {
die "Only one disk is connected! Cannot select $args{disks} disks." ;
}
2020-02-26 22:30:08 +00:00
# For exactly 1 disk, we don't need to do anything.
2017-01-18 07:15:44 +00:00
}
# Handle network disks.
if ( % iscsi ) {
assert_and_click "anaconda_install_destination_add_network_disk" ;
foreach my $ target ( keys % iscsi ) {
my $ ip = $ iscsi { $ target } - > [ 0 ] ;
my $ user = $ iscsi { $ target } - > [ 1 ] ;
my $ password = $ iscsi { $ target } - > [ 2 ] ;
assert_and_click "anaconda_install_destination_add_iscsi_target" ;
wait_still_screen 2 ;
type_safely $ ip ;
wait_screen_change { send_key "tab" ; } ;
type_safely $ target ;
# start discovery - three tabs, enter
type_safely "\t\t\t\n" ;
if ( $ user && $ password ) {
assert_and_click "anaconda_install_destination_target_auth_type" ;
assert_and_click "anaconda_install_destination_target_auth_type_chap" ;
send_key "tab" ;
type_safely $ user ;
send_key "tab" ;
type_safely $ password ;
}
assert_and_click "anaconda_install_destination_target_login" ;
assert_and_click "anaconda_install_destination_select_target" ;
}
assert_and_click "anaconda_spoke_done" ;
}
2017-04-24 12:23:35 +00:00
# If this is a custom partitioning test, select custom partitioning. For testing blivet-gui,
# name of test module should start with custom_blivet_, otherwise it should start with custom_.
if ( get_var ( 'PARTITIONING' ) =~ /^custom_blivet_/ ) {
assert_and_click "anaconda_manual_blivet_partitioning" ;
} elsif ( get_var ( 'PARTITIONING' ) =~ /^custom_/ ) {
2017-01-18 07:15:44 +00:00
assert_and_click "anaconda_manual_partitioning" ;
}
}
sub custom_scheme_select {
# Used for setting custom partitioning scheme (such as LVM).
# Should be called when custom partitioning spoke is displayed.
# Pass the name of the partitioning scheme. Needle
# `anaconda_part_scheme_$scheme` should exist. Example usage:
# `custom_scheme_select("btrfs");` uses needle
# `anaconda_part_scheme_btrfs` to set partition scheme to Btrfs.
my ( $ scheme ) = @ _ ;
assert_and_click "anaconda_part_scheme" ;
# Move the mouse away from the menu
mouse_set ( 10 , 10 ) ;
2018-06-15 20:40:13 +00:00
# workaround for bug aarch64 tests sometimes hit - menu doesn't
# open when clicked. just click it again.
if ( check_screen "anaconda_part_scheme_active" , 5 ) {
assert_and_click "anaconda_part_scheme_active" ;
mouse_set ( 10 , 10 ) ;
}
2017-01-18 07:15:44 +00:00
assert_and_click "anaconda_part_scheme_$scheme" ;
}
2017-04-24 12:23:35 +00:00
sub custom_blivet_add_partition {
# Used to add partition on blivet-gui partitioning screen
# in Anaconda. Should be called when blivet-gui is displayed and free space is selected.
# You can pass device type for partition (needle tagged anaconda_blivet_devicetype_$devicetype should exist),
# whether partitions should be of RAID1 (devicetype is then automatically handled) - you then
2017-05-05 07:29:55 +00:00
# need to have two disks added, size of that partition in MiBs, desired filesystem of that partition
2017-04-24 12:23:35 +00:00
# (anaconda_blivet_part_fs_$filesystem should exist) and mountpoint of that partition (e. g. string "/boot").
my % args = (
devicetype = > "" ,
raid1 = > 0 ,
size = > 0 ,
filesystem = > "" ,
mountpoint = > "" ,
@ _
) ;
$ args { devicetype } = "raid" if $ args { raid1 } ;
assert_and_click "anaconda_blivet_part_add" ;
mouse_set ( 10 , 10 ) ;
if ( $ args { devicetype } ) {
assert_and_click "anaconda_blivet_part_devicetype" ;
mouse_set ( 10 , 10 ) ;
assert_and_click "anaconda_blivet_part_devicetype_$args{devicetype}" ;
}
if ( $ args { raid1 } ) {
# for RAID1, two disks should be selected
send_key "tab" ;
send_key "down" ;
send_key "spc" ;
assert_screen "anaconda_blivet_vdb_selected" ;
assert_and_click "anaconda_blivet_raidlevel_select" ;
mouse_set ( 10 , 10 ) ;
assert_and_click "anaconda_blivet_raidlevel_raid1" ;
}
if ( $ args { size } ) {
2017-05-05 07:29:55 +00:00
assert_and_click "anaconda_blivet_size_unit" ;
assert_and_click "anaconda_blivet_size_unit_mib" ;
send_key "shift-tab" ; # input is one tab back from unit selection listbox
2017-04-24 12:23:35 +00:00
# size input can contain whole set of different values, so we can't match it with needle
type_safely $ args { size } . "\n" ;
}
# if no filesystem was specified or filesystem is already selected, do nothing
if ( $ args { filesystem } && ! check_screen ( "anaconda_blivet_part_fs_$args{filesystem}_selected" , 5 ) ) {
assert_and_click "anaconda_blivet_part_fs" ;
# Move the mouse away from the menu
mouse_set ( 10 , 10 ) ;
assert_and_click "anaconda_blivet_part_fs_$args{filesystem}" ;
}
if ( $ args { mountpoint } ) {
assert_and_click "anaconda_blivet_mountpoint" ;
type_safely $ args { mountpoint } . "\n" ;
}
2020-03-12 00:22:26 +00:00
# seems we can get a lost click here if we click too soon
wait_still_screen 3 ;
2017-04-24 12:23:35 +00:00
assert_and_click "anaconda_blivet_btn_ok" ;
# select "free space" in blivet-gui if it exists, so we could run this function again to add another partition
2020-10-07 22:21:17 +00:00
if ( check_screen ( "anaconda_blivet_free_space" , 15 ) ) {
2017-04-24 12:23:35 +00:00
assert_and_click "anaconda_blivet_free_space" ;
}
}
2021-10-06 04:27:59 +00:00
sub custom_add_partition {
# Used to add partition on Rocky custom-gui partitioning screen
# in Anaconda. Should be called when custom-gui is displayed and free space is selected.
# You can pass device type for partition (needle tagged anaconda_custom_devicetype_$devicetype should exist), size of that partition in MiBs, desired filesystem of that partition
# (anaconda_custom_part_fs_$filesystem should exist) and mountpoint of that partition (e. g. string "/boot").
my % args = (
devicetype = > "" ,
raid1 = > 0 ,
size = > 0 ,
filesystem = > "" ,
mountpoint = > "" ,
@ _
) ;
$ args { devicetype } = "raid" if $ args { raid1 } ;
assert_and_click "anaconda_custom_part_add" ;
if ( $ args { mountpoint } ) {
assert_and_click "anaconda_custom_mountpoint" ;
type_safely $ args { mountpoint } ;
# esc to dismiss the dropdown that appears when we add a mountpoint
send_key "esc" ;
}
if ( $ args { size } ) {
assert_and_click "anaconda_custom_size" ;
# size input can contain whole set of different values, so we can't match it with needle
type_safely $ args { size } ;
}
assert_and_click "anaconda_custom_btn_add_mountpoint" ;
# if ($args{raid1}) {
# # for RAID1, two disks should be selected
# send_key "tab";
# send_key "down";
# send_key "spc";
# assert_screen "anaconda_custom_vdb_selected";
#
# assert_and_click "anaconda_custom_raidlevel_select";
# mouse_set(10, 10);
# assert_and_click "anaconda_custom_raidlevel_raid1";
# }
# if no devicetype was specified or devicetype is already selected, do nothing
2021-10-06 05:06:07 +00:00
if ( ( $ args { devicetype } && ! check_screen ( "anaconda_custom_part_fs_$args{devicetype}_selected" , 5 ) ) ) {
2021-10-06 04:27:59 +00:00
assert_and_click "anaconda_custom_part_devicetype" ;
mouse_set ( 10 , 10 ) ;
assert_and_click "anaconda_custom_part_devicetype_$args{devicetype}" ;
}
# if no filesystem was specified or filesystem is already selected, do nothing
if ( $ args { filesystem } && ! check_screen ( "anaconda_custom_part_fs_$args{filesystem}_selected" , 5 ) ) {
assert_and_click "anaconda_custom_part_fs" ;
assert_and_click "anaconda_custom_part_fs_$args{filesystem}" ;
}
# select "free space" in custom-gui if it exists, so we could run this function again to add another partition
if ( check_screen ( "anaconda_custom_free_space" , 15 ) ) {
assert_and_click "anaconda_custom_free_space" ;
}
}
2020-05-05 09:38:18 +00:00
sub custom_blivet_format_partition {
2021-10-06 04:27:59 +00:00
# This subroutine formats a selected partition. To use it, you must select the
2020-05-05 09:38:18 +00:00
# partition by other means before you format it using this routine.
# You have to create a needle for any non-existing filesystem that is
2020-05-28 09:40:34 +00:00
# passed via the $type, such as anaconda_blivet_part_fs_ext4.
2020-05-05 09:38:18 +00:00
my % args = @ _ ;
# Start editing the partition and select the Format option
assert_and_click "anaconda_blivet_part_edit" ;
assert_and_click "anaconda_blivet_part_format" ;
# Select the appropriate filesystem type.
assert_and_click "anaconda_blivet_part_drop_select" ;
2020-05-27 09:16:42 +00:00
assert_and_click "anaconda_blivet_part_fs_$args{type}" ;
2020-05-05 09:38:18 +00:00
wait_still_screen 2 ;
# Fill in the label if needed.
send_key "tab" ;
if ( $ args { label } ) {
type_very_safely $ args { label } ;
}
# Fill in the mountpoint.
send_key "tab" ;
type_very_safely $ args { mountpoint } ;
assert_and_click "anaconda_blivet_part_format_button" ;
}
sub custom_blivet_resize_partition {
# This subroutine resizes the selected (active) partition to a given value. Note, that
# if the selected value is bigger than the available space, it will only be
2021-10-06 04:27:59 +00:00
# resized to fill up the available space no matter the number.
2020-05-05 09:38:18 +00:00
# This routine cannot will not be able to select a particular partition!!!
my % args = @ _ ;
# Start editing the partition and select the Resize option
assert_and_click "anaconda_blivet_part_edit" ;
assert_and_click "anaconda_blivet_part_resize" ;
# Select the appropriate units. Note, that there must a be needle existing
2021-10-06 04:27:59 +00:00
# for each possible unit that you might want to use, such as
2020-05-05 09:38:18 +00:00
# "anaconda_blivet_size_unit_gib".
assert_and_click "anaconda_blivet_part_drop_select" ;
assert_and_click "anaconda_blivet_size_unit_$args{units}" ;
# Move back to the value field.
send_key "shift-tab" ;
# Type in the new size.
type_very_safely $ args { size } ;
assert_and_click "anaconda_blivet_part_resize_button" ;
}
2017-01-18 07:15:44 +00:00
sub custom_change_type {
# Used to set different device types for specified partition (e.g.
# RAID). Should be called when custom partitioning spoke is
# displayed. Pass it type of partition and name of partition.
# Needles `anaconda_part_select_$part` and
# `anaconda_part_device_type_$type` should exist. Example usage:
# `custom_change_type("raid", "root");` uses
# `anaconda_part_select_root` and `anaconda_part_device_type_raid`
# needles to set RAID for root partition.
my ( $ type , $ part ) = @ _ ;
$ part || = "root" ;
assert_and_click "anaconda_part_select_$part" ;
assert_and_click "anaconda_part_device_type" ;
# Move the mouse away from the menu
mouse_set ( 10 , 10 ) ;
assert_and_click "anaconda_part_device_type_$type" ;
assert_and_click "anaconda_part_update_settings" ;
2021-03-17 21:19:20 +00:00
wait_still_screen 5 ;
2017-01-18 07:15:44 +00:00
}
sub custom_change_fs {
# Used to set different file systems for specified partition.
# Should be called when custom partitioning spoke is displayed.
# Pass filesystem name and name of partition. Needles
# `anaconda_part_select_$part` and `anaconda_part_fs_$fs` should
# exist. Example usage:
# `custom_change_fs("ext3", "root");` uses
# `anaconda_part_select_root` and `anaconda_part_fs_ext3` needles
# to set ext3 file system for root partition.
my ( $ fs , $ part ) = @ _ ;
$ part || = "root" ;
assert_and_click "anaconda_part_select_$part" ;
2020-12-05 00:15:33 +00:00
wait_still_screen 5 ;
2017-01-18 07:15:44 +00:00
# if fs is already set correctly, do nothing
return if ( check_screen "anaconda_part_fs_${fs}_selected" , 5 ) ;
assert_and_click "anaconda_part_fs" ;
# Move the mouse away from the menu
mouse_set ( 10 , 10 ) ;
assert_and_click "anaconda_part_fs_$fs" ;
assert_and_click "anaconda_part_update_settings" ;
2021-03-17 21:19:20 +00:00
wait_still_screen 5 ;
2017-01-18 07:15:44 +00:00
}
sub custom_change_device {
my ( $ part , $ devices ) = @ _ ;
assert_and_click "anaconda_part_select_$part" ;
assert_and_click "anaconda_part_device_modify" ;
foreach my $ device ( split ( / / , $ devices ) ) {
assert_and_click "anaconda_part_device_${device}" ;
}
assert_and_click "anaconda_part_device_select" ;
assert_and_click "anaconda_part_update_settings" ;
2021-03-17 21:19:20 +00:00
wait_still_screen 5 ;
2017-01-18 07:15:44 +00:00
}
sub custom_delete_part {
# Used for deletion of previously added partitions in custom
# partitioning spoke. Should be called when custom partitioning
# spoke is displayed. Pass the partition name. Needle
# `anaconda_part_select_$part` should exist. Example usage:
# `custom_delete_part('swap');` uses needle
# `anaconda_part_select_swap` to delete previously added swap
# partition.
my ( $ part ) = @ _ ;
return if not $ part ;
assert_and_click "anaconda_part_select_$part" ;
assert_and_click "anaconda_part_delete" ;
}
sub get_full_repo {
my ( $ repourl ) = @ _ ;
# trivial thing we kept repeating: fill out an HTTP or HTTPS
2019-02-04 14:46:31 +00:00
# repo URL with flavor and arch, leave hd & NFS ones alone
# (as for those tests we just use a mounted ISO and URL is complete)
if ( $ repourl !~ m/^(nfs|hd:)/ ) {
2017-11-07 00:25:40 +00:00
# Everything variant doesn't exist for modular composes atm,
# only Server
my $ variant = 'Everything' ;
$ variant = 'Server' if ( get_var ( "MODULAR" ) ) ;
$ repourl . = "/${variant}/" . get_var ( "ARCH" ) . "/os" ;
2017-01-18 07:15:44 +00:00
}
return $ repourl ;
}
sub get_mirrorlist_url {
2021-08-24 13:41:44 +00:00
return "mirrors.rockylinux.org/mirrorlist?repo=rocky-BaseOS-" . lc ( get_var ( "VERSION" ) ) . "&arch=" . get_var ( 'ARCH' ) ;
2017-01-18 07:15:44 +00:00
}
2021-05-03 15:37:46 +00:00
sub check_help_on_pane {
# This subroutine opens the selected Anaconda pane and checks
# if the Help button can be clicked to obtain relevant help.
#
# Pass an argument to select particular pane to check.
my $ screen = shift ;
# Some Help buttons need to be accessed directly according
# to various installation steps (and not from the main hub),
# namely the Main hub Help button, Language selection Help button
# and Installation progress Help button. For the aforementioned
# step, we are skipping selecting the panes.
if ( $ screen ne "main" && $ screen ne "language_selection" && $ screen ne "installation_progress" ) {
2021-05-31 22:03:17 +00:00
send_key_until_needlematch ( "anaconda_main_hub_$screen" , "shift-tab" ) ;
2021-05-31 22:27:11 +00:00
wait_screen_change { click_lastmatch ; } ;
2021-05-03 15:37:46 +00:00
}
# For Help, click on the the Help button.
assert_and_click "anaconda_help_button" ;
# On the main hub, the Help summary is shown, from where a link
# takes us to Installation progress. This is a specific situation,
# so let's handle this differently.
if ( $ screen eq "main" ) {
# Check the Installation Summary screen.
assert_screen "anaconda_help_summary" ;
# Click on Installation Progress link
assert_and_click "anaconda_help_progress_link" ;
# Check the Installation Progress screen
assert_screen "anaconda_help_progress" ;
}
# Otherwise, only check the relevant screen.
else {
assert_screen "anaconda_help_$screen" ;
}
# Close Help window
assert_and_click "anaconda_help_quit" ;
# Where panes were not opened, we will not close them.
if ( $ screen ne "main" && $ screen ne "language_selection" && $ screen ne "installation_progress" ) {
assert_and_click "anaconda_spoke_done" ;
}
# In the situation, when we do not arrive at main hub, we will skip
# testing that main hub is shown.
if ( $ screen ne "language_selection" && $ screen ne "installation_progress" ) {
2021-05-31 22:03:17 +00:00
# on leaving a spoke, it is highlighted on the main hub, which
# can throw off the match here. so we'll try hitting shift-tab
# a few times to shift focus
send_key_until_needlematch ( "anaconda_main_hub" , "shift-tab" ) ;
2021-05-31 22:45:05 +00:00
# things can take some time to settle after this one
wait_still_screen 10 if ( $ screen eq "install_destination" ) ;
2021-05-03 15:37:46 +00:00
}
}
2021-04-27 11:02:53 +00:00
sub crash_anaconda_text {
# This routine uses the Anaconda crash trigger to break the ongoing Anaconda installation to simulate
# an Anaconda crash and runs a series of steps that results in creating a bug in Bugzilla.
# It is used in the `install_text.pm` test and can be switched on by using the CRASH_REPORT
2021-10-06 04:27:59 +00:00
# variable set to 1.
2021-04-27 11:02:53 +00:00
#
# First let us navigate to reach the shell window in Anaconda using the alt-f3 combo,
# this should take us to another terminal, where we can simulate the crash.
send_key "alt-f3" ;
assert_screen ( "anaconda_text_install_shell" ) ;
# We use the trigger command to do the simulated crash.
type_string "kill -USR1 `cat /var/run/anaconda.pid`\n" ;
# And navigate back to the main panel of Anaconda. This should require
send_key "alt-f1" ;
assert_screen ( "anaconda_text_install_main" ) ;
# We wait until the crash menu appears. This usually takes some time,
# so let's try for 300 seconds, this should be long enough.
my $ trials = 1 ;
until ( check_screen ( "anaconda_text_crash_menu_ready" ) || $ trials > 30 ) {
sleep 10 ;
+ + $ trials ;
}
# If the crash menu never appears, let's assert it to fail.
if ( $ trials > 30 ) {
assert_screen ( "anaconda_text_crash_menu_ready" ) ;
}
}
sub report_bug_text {
# This routine handles the Bugzilla reporting after a simulated crash on
# a textual console.
# We will not create a needle for every menu item, and we will fail,
# if there will be no positive Bugzilla confirmation shown at the end
2021-10-06 04:27:59 +00:00
# of the process and then we will fail.
2021-04-27 11:02:53 +00:00
#
# Let us record the time of this test run. Later, we will use it to
# limit the Bugzilla search.
my $ timestamp = time ( ) ;
#
# First, collect the credentials.
my $ login = get_var ( "BUGZILLA_LOGIN" ) ;
my $ password = get_var ( "_SECRET_BUGZILLA_PASSWORD" ) ;
my $ apikey = get_var ( "_SECRET_BUGZILLA_APIKEY" ) ;
# Choose item 1 - Report the bug.
type_string "1\n" ;
sleep 2 ;
# Choose item 1 - Report to Bugzilla
type_string "1\n" ;
sleep 5 ;
# Do login.
type_string $ login ;
type_string "\n" ;
sleep 5 ;
# Enter the name of the Zilla.
type_password $ password ;
type_string "\n" ;
sleep 10 ;
2021-10-06 04:27:59 +00:00
# Save the report without changing it.
2021-04-27 11:02:53 +00:00
# It would need some more tweaking to actually type into the report, but since
# it is reported even if unchanged, we leave it as such.
type_string ":wq\n" ;
# Wait until the Crash menu appears again.
2021-10-06 04:27:59 +00:00
# The same screen shows the result of the Bugzilla operation,
2021-04-27 11:02:53 +00:00
# so if the needle matches, the bug has been created in Bugzilla.
# Bugzilla connection is slow so we need to wait out some time,
# therefore let's use a cycle that will check each 10 seconds and
# ends if there is no correct answer from Bugzilla in 120 seconds.
my $ counter = 0 ;
until ( check_screen ( "anaconda_text_bug_reported" ) || $ counter > 12 ) {
sleep 10 ;
+ + $ counter ;
}
# Sometimes, Bugzilla throws out a communication error although the bug has been
2021-10-06 04:27:59 +00:00
# created successfully. If this happens, we will softfail and leave the creation
2021-04-27 11:02:53 +00:00
# check to a later step.
if ( $ counter > 12 ) {
record_soft_failure "Warning: Bugzilla has reported an error which could mean that the bug has not been created correctly, but it probably is not a real problem, if the test has not failed completely. " ;
}
# Now, let us check with Bugzilla directly, if the bug has been created.
# First, we shall get a Bugzilla format timestamp to use it in the query.
2021-10-06 04:27:59 +00:00
# The timestamp will limit the list of bugs to those that have been created since
2021-04-27 11:02:53 +00:00
# the then -> resulting with high probability in the one that this test run
# has just created.
$ timestamp = convert_to_bz_timestamp ( $ timestamp ) ;
# Then we fetch the latest bug from Bugzilla.
my $ lastbug = get_newest_bug ( $ timestamp , $ login ) ;
unless ( $ lastbug ) {
die "Bugzilla returned no newly created bug. It seems that the bug has not been created." ;
}
else {
print ( "BUGZILLA: The last bug was found: $lastbug\n" ) ;
}
2021-10-06 04:27:59 +00:00
# We have found that the bug indeed is in the bugzilla (otherwise
2021-04-27 11:02:53 +00:00
# we would have died already) so now we close it to clean up after this test run.
my $ result = close_notabug ( $ lastbug , $ apikey ) ;
unless ( $ result ) {
record_soft_failure "The bug has not been closed for some reason. Check manually." ;
}
else {
print ( "BUGZILLA: The last bug $lastbug changed status to CLOSED.\n" ) ;
}
# Quit anaconda
type_string "4\n" ;
2021-10-06 04:27:59 +00:00
2021-04-27 11:02:53 +00:00
}