mirror of
https://git.proxmox.com/git/qemu-server
synced 2025-05-02 05:37:19 +00:00

and the associated parts for 'qm start'. Each test will first populate the MigrationTest/run directory with the relevant configuration files and files keeping track of the state of everything necessary. Second, the mock-script for migration is executed, which in turn will execute the 'qm start' mock-script (if it's an online test that gets far enough). The scripts will simulate a migration and update the relevant files in the MigrationTest/run directory. Finally, the main test script will evaluate the state. The main checks are the volume IDs on the source and target and the VM configuration itself. Additional checks are the vm_status and expected_calls, keeping track if certain calls have been made. The rationale behind creating two mock-scripts is two-fold: 1. It removes the need to hard code responses for the tunnel and to recycle logic for determining and allocating migration volumes. Some of that logic already happens in the API part, so it was necessary to mock the whole CLI-Handler. 2. It allows testing the code relevant for migration in 'qm start' as well, and it should even be possible to test different versions of the mock-scripts against each other. With a bit of extra work and things like 'git worktree', it might even be possible to automate this. The helper get_patched config is useful to change pre-defined configuration files on the fly, avoiding the new to explicitly define whole configurations to test for something in many cases. Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
320 lines
9.6 KiB
Perl
320 lines
9.6 KiB
Perl
package MigrationTest::QemuMigrateMock;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use JSON;
|
|
use Test::MockModule;
|
|
|
|
use MigrationTest::Shared;
|
|
|
|
use PVE::API2::Qemu;
|
|
use PVE::Storage;
|
|
use PVE::Tools qw(file_set_contents file_get_contents);
|
|
|
|
use PVE::CLIHandler;
|
|
use base qw(PVE::CLIHandler);
|
|
|
|
my $RUN_DIR_PATH = $ENV{RUN_DIR_PATH} or die "no RUN_DIR_PATH set\n";
|
|
my $QM_LIB_PATH = $ENV{QM_LIB_PATH} or die "no QM_LIB_PATH set\n";
|
|
|
|
my $source_volids = decode_json(file_get_contents("${RUN_DIR_PATH}/source_volids"));
|
|
my $source_vdisks = decode_json(file_get_contents("${RUN_DIR_PATH}/source_vdisks"));
|
|
my $vm_status = decode_json(file_get_contents("${RUN_DIR_PATH}/vm_status"));
|
|
my $expected_calls = decode_json(file_get_contents("${RUN_DIR_PATH}/expected_calls"));
|
|
my $fail_config = decode_json(file_get_contents("${RUN_DIR_PATH}/fail_config"));
|
|
my $storage_migrate_map = decode_json(file_get_contents("${RUN_DIR_PATH}/storage_migrate_map"));
|
|
my $migrate_params = decode_json(file_get_contents("${RUN_DIR_PATH}/migrate_params"));
|
|
|
|
my $test_vmid = $migrate_params->{vmid};
|
|
my $test_target = $migrate_params->{target};
|
|
my $test_opts = $migrate_params->{opts};
|
|
my $current_log = '';
|
|
|
|
my $vm_stop_executed = 0;
|
|
|
|
# mocked modules
|
|
|
|
my $inotify_module = Test::MockModule->new("PVE::INotify");
|
|
$inotify_module->mock(
|
|
nodename => sub {
|
|
return 'pve0';
|
|
},
|
|
);
|
|
|
|
$MigrationTest::Shared::qemu_config_module->mock(
|
|
move_config_to_node => sub {
|
|
my ($self, $vmid, $target) = @_;
|
|
die "moving wrong config: '$vmid'\n" if $vmid ne $test_vmid;
|
|
die "moving config to wrong node: '$target'\n" if $target ne $test_target;
|
|
delete $expected_calls->{move_config_to_node};
|
|
},
|
|
);
|
|
|
|
my $qemu_migrate_module = Test::MockModule->new("PVE::QemuMigrate");
|
|
$qemu_migrate_module->mock(
|
|
finish_tunnel => sub {
|
|
delete $expected_calls->{'finish_tunnel'};
|
|
return;
|
|
},
|
|
fork_tunnel => sub {
|
|
die "fork_tunnel (mocked) - implement me\n"; # currently no call should lead here
|
|
},
|
|
read_tunnel => sub {
|
|
die "read_tunnel (mocked) - implement me\n"; # currently no call should lead here
|
|
},
|
|
start_remote_tunnel => sub {
|
|
my ($self, $raddr, $rport, $ruri, $unix_socket_info) = @_;
|
|
$expected_calls->{'finish_tunnel'} = 1;
|
|
$self->{tunnel} = {
|
|
writer => "mocked",
|
|
reader => "mocked",
|
|
pid => 123456,
|
|
version => 1,
|
|
};
|
|
},
|
|
write_tunnel => sub {
|
|
my ($self, $tunnel, $timeout, $command) = @_;
|
|
|
|
if ($command =~ m/^resume (\d+)$/) {
|
|
my $vmid = $1;
|
|
die "resuming wrong VM '$vmid'\n" if $vmid ne $test_vmid;
|
|
return;
|
|
}
|
|
die "write_tunnel (mocked) - implement me: $command\n";
|
|
},
|
|
log => sub {
|
|
my ($self, $level, $message) = @_;
|
|
$current_log .= "$level: $message\n";
|
|
},
|
|
mon_cmd => sub {
|
|
my ($vmid, $command, %params) = @_;
|
|
|
|
if ($command eq 'nbd-server-start') {
|
|
return;
|
|
} elsif ($command eq 'block-dirty-bitmap-add') {
|
|
my $drive = $params{node};
|
|
delete $expected_calls->{"block-dirty-bitmap-add-${drive}"};
|
|
return;
|
|
} elsif ($command eq 'block-dirty-bitmap-remove') {
|
|
return;
|
|
} elsif ($command eq 'query-migrate') {
|
|
return { status => 'failed' } if $fail_config->{'query-migrate'};
|
|
return { status => 'completed' };
|
|
} elsif ($command eq 'migrate') {
|
|
return;
|
|
} elsif ($command eq 'migrate-set-parameters') {
|
|
return;
|
|
} elsif ($command eq 'migrate_cancel') {
|
|
return;
|
|
}
|
|
die "mon_cmd (mocked) - implement me: $command";
|
|
},
|
|
transfer_replication_state => sub {
|
|
delete $expected_calls->{transfer_replication_state};
|
|
},
|
|
switch_replication_job_target => sub {
|
|
delete $expected_calls->{switch_replication_job_target};
|
|
},
|
|
);
|
|
|
|
$MigrationTest::Shared::qemu_server_module->mock(
|
|
kvm_user_version => sub {
|
|
return "5.0.0";
|
|
},
|
|
qemu_blockjobs_cancel => sub {
|
|
return;
|
|
},
|
|
qemu_drive_mirror => sub {
|
|
my ($vmid, $drive, $dst_volid, $vmiddst, $is_zero_initialized, $jobs, $completion, $qga, $bwlimit, $src_bitmap) = @_;
|
|
|
|
die "drive_mirror with wrong vmid: '$vmid'\n" if $vmid ne $test_vmid;
|
|
die "qemu_drive_mirror '$drive' error\n" if $fail_config->{qemu_drive_mirror}
|
|
&& $fail_config->{qemu_drive_mirror} eq $drive;
|
|
|
|
my $nbd_info = decode_json(file_get_contents("${RUN_DIR_PATH}/nbd_info"));
|
|
die "target does not expect drive mirror for '$drive'\n"
|
|
if !defined($nbd_info->{$drive});
|
|
delete $nbd_info->{$drive};
|
|
file_set_contents("${RUN_DIR_PATH}/nbd_info", to_json($nbd_info));
|
|
},
|
|
qemu_drive_mirror_monitor => sub {
|
|
return;
|
|
},
|
|
set_migration_caps => sub {
|
|
return;
|
|
},
|
|
vm_stop => sub {
|
|
$vm_stop_executed = 1;
|
|
delete $expected_calls->{'vm_stop'};
|
|
},
|
|
);
|
|
|
|
my $qemu_server_cpuconfig_module = Test::MockModule->new("PVE::QemuServer::CPUConfig");
|
|
$qemu_server_cpuconfig_module->mock(
|
|
get_cpu_from_running_vm => sub {
|
|
die "invalid test: if you specify a custom CPU model you need to " .
|
|
"specify runningcpu as well\n" if !defined($vm_status->{runningcpu});
|
|
return $vm_status->{runningcpu};
|
|
}
|
|
);
|
|
|
|
my $qemu_server_helpers_module = Test::MockModule->new("PVE::QemuServer::Helpers");
|
|
$qemu_server_helpers_module->mock(
|
|
vm_running_locally => sub {
|
|
return $vm_status->{running} && !$vm_stop_executed;
|
|
},
|
|
);
|
|
|
|
my $qemu_server_machine_module = Test::MockModule->new("PVE::QemuServer::Machine");
|
|
$qemu_server_machine_module->mock(
|
|
qemu_machine_pxe => sub {
|
|
die "invalid test: no runningmachine specified\n"
|
|
if !defined($vm_status->{runningmachine});
|
|
return $vm_status->{runningmachine};
|
|
},
|
|
);
|
|
|
|
my $ssh_info_module = Test::MockModule->new("PVE::SSHInfo");
|
|
$ssh_info_module->mock(
|
|
get_ssh_info => sub {
|
|
my ($node, $network_cidr) = @_;
|
|
return {
|
|
ip => '1.2.3.4',
|
|
name => $node,
|
|
network => $network_cidr,
|
|
};
|
|
},
|
|
);
|
|
|
|
$MigrationTest::Shared::storage_module->mock(
|
|
storage_migrate => sub {
|
|
my ($cfg, $volid, $target_sshinfo, $target_storeid, $opts, $logfunc) = @_;
|
|
|
|
die "storage_migrate '$volid' error\n" if $fail_config->{storage_migrate}
|
|
&& $fail_config->{storage_migrate} eq $volid;
|
|
|
|
my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
|
|
|
|
die "invalid test: need to add entry for '$volid' to storage_migrate_map\n"
|
|
if $storeid ne $target_storeid && !defined($storage_migrate_map->{$volid});
|
|
|
|
my $target_volname = $storage_migrate_map->{$volid} // $opts->{target_volname} // $volname;
|
|
my $target_volid = "${target_storeid}:${target_volname}";
|
|
MigrationTest::Shared::add_target_volid($target_volid);
|
|
|
|
return $target_volid;
|
|
},
|
|
vdisk_list => sub { # expects vmid to be set
|
|
my ($cfg, $storeid, $vmid, $vollist) = @_;
|
|
|
|
my @storeids = defined($storeid) ? ($storeid) : keys %{$source_vdisks};
|
|
|
|
my $res = {};
|
|
foreach my $storeid (@storeids) {
|
|
my $list_for_storeid = $source_vdisks->{$storeid};
|
|
my @list_for_vm = grep { $_->{vmid} eq $vmid } @{$list_for_storeid};
|
|
$res->{$storeid} = \@list_for_vm;
|
|
}
|
|
return $res;
|
|
},
|
|
vdisk_free => sub {
|
|
my ($scfg, $volid) = @_;
|
|
|
|
die "vdisk_free '$volid' error\n" if defined($fail_config->{vdisk_free})
|
|
&& $fail_config->{vdisk_free} eq $volid;
|
|
|
|
delete $source_volids->{$volid};
|
|
},
|
|
);
|
|
|
|
$MigrationTest::Shared::tools_module->mock(
|
|
get_host_address_family => sub {
|
|
die "get_host_address_family (mocked) - implement me\n"; # currently no call should lead here
|
|
},
|
|
next_migrate_port => sub {
|
|
die "next_migrate_port (mocked) - implement me\n"; # currently no call should lead here
|
|
},
|
|
run_command => sub {
|
|
my ($cmd_tail, %param) = @_;
|
|
|
|
my $cmd_msg = to_json($cmd_tail);
|
|
|
|
my $cmd = shift @{$cmd_tail};
|
|
|
|
if ($cmd eq '/usr/bin/ssh') {
|
|
while (scalar(@{$cmd_tail})) {
|
|
$cmd = shift @{$cmd_tail};
|
|
if ($cmd eq '/bin/true') {
|
|
return 0;
|
|
} elsif ($cmd eq 'qm') {
|
|
$cmd = shift @{$cmd_tail};
|
|
if ($cmd eq 'start') {
|
|
delete $expected_calls->{ssh_qm_start};
|
|
|
|
delete $vm_status->{runningmachine};
|
|
delete $vm_status->{runningcpu};
|
|
|
|
my @options = ( @{$cmd_tail} );
|
|
while (scalar(@options)) {
|
|
my $opt = shift @options;
|
|
if ($opt eq '--machine') {
|
|
$vm_status->{runningmachine} = shift @options;
|
|
} elsif ($opt eq '--force-cpu') {
|
|
$vm_status->{runningcpu} = shift @options;
|
|
}
|
|
}
|
|
|
|
return $MigrationTest::Shared::tools_module->original('run_command')->([
|
|
'/usr/bin/perl',
|
|
"-I${QM_LIB_PATH}",
|
|
"-I${QM_LIB_PATH}/test",
|
|
"${QM_LIB_PATH}/test/MigrationTest/QmMock.pm",
|
|
'start',
|
|
@{$cmd_tail},
|
|
], %param);
|
|
|
|
} elsif ($cmd eq 'nbdstop') {
|
|
delete $expected_calls->{ssh_nbdstop};
|
|
return 0;
|
|
} elsif ($cmd eq 'resume') {
|
|
return 0;
|
|
} elsif ($cmd eq 'unlock') {
|
|
my $vmid = shift @{$cmd_tail};;
|
|
die "unlocking wrong vmid: $vmid\n" if $vmid ne $test_vmid;
|
|
PVE::QemuConfig->remove_lock($vmid);
|
|
return 0;
|
|
} elsif ($cmd eq 'stop') {
|
|
return 0;
|
|
}
|
|
die "run_command (mocked) ssh qm command - implement me: ${cmd_msg}";
|
|
} elsif ($cmd eq 'pvesm') {
|
|
$cmd = shift @{$cmd_tail};
|
|
if ($cmd eq 'free') {
|
|
my $volid = shift @{$cmd_tail};
|
|
return 1 if $fail_config->{ssh_pvesm_free}
|
|
&& $fail_config->{ssh_pvesm_free} eq $volid;
|
|
MigrationTest::Shared::remove_target_volid($volid);
|
|
return 0;
|
|
}
|
|
die "run_command (mocked) ssh pvesm command - implement me: ${cmd_msg}";
|
|
}
|
|
}
|
|
die "run_command (mocked) ssh command - implement me: ${cmd_msg}";
|
|
}
|
|
die "run_command (mocked) - implement me: ${cmd_msg}";
|
|
},
|
|
);
|
|
|
|
eval { PVE::QemuMigrate->migrate($test_target, undef, $test_vmid, $test_opts) };
|
|
my $error = $@;
|
|
|
|
file_set_contents("${RUN_DIR_PATH}/source_volids", to_json($source_volids));
|
|
file_set_contents("${RUN_DIR_PATH}/vm_status", to_json($vm_status));
|
|
file_set_contents("${RUN_DIR_PATH}/expected_calls", to_json($expected_calls));
|
|
file_set_contents("${RUN_DIR_PATH}/log", $current_log);
|
|
|
|
die $error if $error;
|
|
|
|
1;
|