diff --git a/xCAT-server/lib/xcat/plugins/kvm.pm b/xCAT-server/lib/xcat/plugins/kvm.pm index 1e81317ae..0bae3a15e 100644 --- a/xCAT-server/lib/xcat/plugins/kvm.pm +++ b/xCAT-server/lib/xcat/plugins/kvm.pm @@ -498,6 +498,8 @@ sub build_diskstruct { my @suffixes = ('a', 'b', 'd' .. 'zzz'); my $suffidx = 0; my $storagemodel = $confdata->{vm}->{$node}->[0]->{storagemodel}; + my $profile = guest_arch_profile($confdata->{nodetype}->{$node}->[0]->{arch}, + $confdata->{ $confdata->{vm}->{$node}->[0]->{host} }->{cpumodel}); my $cachemethod = "none"; if ($confdata->{vm}->{$node}->[0]->{storagecache}) { $cachemethod = $confdata->{vm}->{$node}->[0]->{storagecache}; @@ -517,7 +519,7 @@ sub build_diskstruct { #if not defined, model will stay undefined like above $model = $storagemodel; - unless ($model) { $model = 'ide'; } #if still not defined, ide + unless ($model) { $model = $profile->{disk_model}; } } my $prefix = 'hd'; if ($model eq 'virtio') { @@ -586,7 +588,8 @@ sub build_diskstruct { push @returns, $diskhash; } } - my $cdprefix = 'hd'; + # The riscv64 virt machine has no IDE controller, so the optical drive is scsi there. + my $cdprefix = $profile->{cd_prefix}; # Normally for vmstoragemodel=virtio, we would set prefix of "vd", but device name vd* # doesn't work for CDROM, so for now use the same prefix "sd" as for vmstoragemodel=scsi. @@ -704,6 +707,54 @@ sub getUnits { } } +# guest_arch_profile: the libvirt domain type and settings for one guest. +# +# The architecture of the guest comes from the node, not from the hypervisor. A node whose +# arch is not the arch of the hypervisor runs under emulation, which libvirt expresses as +# domain type "qemu". riscv64 has no BIOS: the virt machine boots UEFI, and pae/acpi/apic +# are x86 features that libvirt rejects there. +# +# POWER keeps reading the hypervisor cpumodel. ppc64le hypervisors report "ppc64le" (not +# "ppc64"); both are pseries guests whose libvirt arch is "ppc64". +# +# arch and machine stay undef when libvirt is to use its own default for the hypervisor. +sub guest_arch_profile { + my ($guest_arch, $hyp_cpumodel) = @_; + my %profile = ( + domtype => 'kvm', + arch => undef, + machine => undef, + firmware => undef, + x86_features => 1, + bios => 1, + sound => 1, + video => 'vga', + usb_input => 1, + disk_model => 'ide', + cd_prefix => 'hd', + ); + if (defined($guest_arch) and $guest_arch eq 'riscv64') { + $profile{domtype} = 'qemu'; + $profile{arch} = 'riscv64'; + $profile{machine} = 'virt'; + $profile{firmware} = 'efi'; + $profile{x86_features} = 0; + $profile{bios} = 0; + $profile{sound} = 0; + $profile{video} = 'virtio'; + $profile{usb_input} = 0; + $profile{disk_model} = 'scsi'; + $profile{cd_prefix} = 'sd'; + } elsif (defined($hyp_cpumodel) and ($hyp_cpumodel eq "ppc64" or $hyp_cpumodel eq "ppc64le")) { + $profile{arch} = 'ppc64'; + $profile{machine} = 'pseries'; + $profile{x86_features} = 0; + $profile{bios} = 0; + $profile{sound} = 0; + } + return \%profile; +} + sub build_xmldesc { my $node = shift; my %args = @_; @@ -716,19 +767,16 @@ sub build_xmldesc { $hypcputhreads = "1"; } - $xtree{type} = 'kvm'; + my $profile = guest_arch_profile($confdata->{nodetype}->{$node}->[0]->{arch}, $hypcpumodel); + + $xtree{type} = $profile->{domtype}; $xtree{name}->{content} = $node; $xtree{uuid}->{content} = getNodeUUID($node); $xtree{os} = build_oshash(); - # ppc64le hypervisors report cpumodel "ppc64le" (not "ppc64"); both are pseries - # guests whose libvirt arch is "ppc64". Without this the guest is emitted - # as an x86-style domain (no machine, plus the pae/acpi/apic below) which libvirt - # rejects on ppc64le hosts: "machine type 'pseries-*' does not support ACPI". - if (defined($hypcpumodel) and ($hypcpumodel eq "ppc64" or $hypcpumodel eq "ppc64le")) { - $xtree{os}->{type}->{arch} = "ppc64"; - $xtree{os}->{type}->{machine} = "pseries"; - delete $xtree{os}->{bios}; - } + $xtree{os}->{type}->{arch} = $profile->{arch} if defined $profile->{arch}; + $xtree{os}->{type}->{machine} = $profile->{machine} if defined $profile->{machine}; + $xtree{os}->{firmware} = $profile->{firmware} if defined $profile->{firmware}; + delete $xtree{os}->{bios} unless $profile->{bios}; if ($args{memory}) { $xtree{memory}->{content} = getUnits($args{memory}, "M", 1024); if ($confdata->{vm}->{$node}->[0]->{memory}) { @@ -940,9 +988,7 @@ sub build_xmldesc { } } - # pae/acpi/apic are x86 features; pseries (ppc64/ppc64le) guests do not support - # them and libvirt rejects the domain if they are present. - unless (defined($hypcpumodel) and ($hypcpumodel eq "ppc64" or $hypcpumodel eq "ppc64le")) { + if ($profile->{x86_features}) { $xtree{features}->{pae} = {}; $xtree{features}->{acpi} = {}; $xtree{features}->{apic} = {}; @@ -965,10 +1011,13 @@ sub build_xmldesc { $vram = 65536; } #surprise, spice blows up with less vram than this after version 0.6 and up $xtree{devices}->{video} = [ { 'content' => '', 'model' => { type => $model, vram => $vram } } ]; } else { - $xtree{devices}->{video} = [ { 'content' => '', 'model' => { type => 'vga', vram => 8192 } } ]; + $xtree{devices}->{video} = [ { 'content' => '', 'model' => { type => $profile->{video}, vram => 8192 } } ]; + } + # The riscv64 virt machine has no USB controller, and libvirt refuses a USB device there. + if ($profile->{usb_input}) { + $xtree{devices}->{input}->{type} = 'tablet'; + $xtree{devices}->{input}->{bus} = 'usb'; } - $xtree{devices}->{input}->{type} = 'tablet'; - $xtree{devices}->{input}->{bus} = 'usb'; if (defined($confdata->{vm}->{$node}->[0]->{vidproto})) { $xtree{devices}->{graphics}->{type} = $confdata->{vm}->{$node}->[0]->{vidproto}; } else { @@ -983,10 +1032,9 @@ sub build_xmldesc { } if (defined($hypcpumodel) and $hypcpumodel eq 'ppc64') { $xtree{devices}->{emulator}->{content} = "/usr/bin/qemu-system-ppc64"; - } elsif (defined($hypcpumodel) and $hypcpumodel eq 'ppc64le') { - # do nothing for ppc64le, do not support sound at this time - ; - } else { + } + # libvirt resolves the emulator for every other architecture from its own capabilities. + if ($profile->{sound}) { $xtree{devices}->{sound}->{model} = 'ich6'; } diff --git a/xCAT-test/unit/kvm_guest_arch.t b/xCAT-test/unit/kvm_guest_arch.t new file mode 100644 index 000000000..763722ef7 --- /dev/null +++ b/xCAT-test/unit/kvm_guest_arch.t @@ -0,0 +1,122 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use Test::More; + +# The scratch package below declares these; the test names them once each. +no warnings 'once'; + +my $source = "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/kvm.pm"; +open(my $source_fh, '<', $source) or die "open $source: $!"; +my $content = do { local $/; <$source_fh> }; +close($source_fh) or die "close $source: $!"; + +my @routines; +for my $name (qw(build_xmldesc guest_arch_profile build_oshash build_diskstruct getUnits)) { + my ($routine) = $content =~ /^(sub \Q$name\E\s*\{.*?^\})/ms; + BAIL_OUT("could not extract $name from kvm.pm") unless $routine; + push(@routines, $routine); +} + +# kvm.pm needs a management node to load, so the domain builder runs in a scratch package. +# Only the routines that reach libvirt or the xCAT database are replaced; the domain builder +# itself is the code under test. +my $harness = <<'PERL'; +package KVMArch; +use XML::Simple qw(XMLout); +our ($node, $confdata, $updatetable, $hypconn); +sub getNodeUUID { return '00000000-0000-0000-0000-000000000001'; } +sub get_multiple_paths_by_url { return {}; } +sub build_nicstruct { return []; } +sub genpassword { return 'password'; } +PERL + +eval $harness . join("\n", @routines) . "\n1;\n"; ## no critic (BuiltinFunctions::ProhibitStringyEval) +BAIL_OUT("could not load the kvm domain builder: $@") if $@; + +# Build one domain for a node of $guest_arch on a hypervisor that reports $hyp_cpumodel. +sub domain_xml { + my ($guest_arch, $hyp_cpumodel) = @_; + local $KVMArch::node = 'cn1'; + local $KVMArch::confdata = { + vm => { cn1 => [ { host => 'hyp1', memory => 8192, cpus => 4 } ] }, + nodetype => { cn1 => [ { arch => $guest_arch, os => 'rocky10.2' } ] }, + hyp1 => { cpumodel => $hyp_cpumodel }, + }; + local $KVMArch::updatetable = {}; + my $xml = KVMArch::build_xmldesc('cn1'); + BAIL_OUT("build_xmldesc returned no XML for $guest_arch on $hyp_cpumodel") + unless defined $xml and !ref $xml; + return $xml; +} + +sub os_type_element { + my ($xml) = @_; + my ($attrs) = $xml =~ m{]*)>hvm}s; + return defined $attrs ? $attrs : ''; +} + +# A riscv64 node on an x86_64 hypervisor. The guest architecture is not the host +# architecture, so the domain runs under emulation and states its own machine type. +my $riscv = domain_xml('riscv64', 'x86_64'); +like($riscv, qr/]*\btype="qemu"/, + 'a riscv64 guest on an x86_64 hypervisor is a qemu domain, not kvm'); +like(os_type_element($riscv), qr/\barch="riscv64"/, + 'the domain arch is the arch of the node'); +like(os_type_element($riscv), qr/\bmachine="virt"/, + 'a riscv64 guest uses the virt machine type'); +like($riscv, qr/]*\bfirmware="efi"/, + 'a riscv64 virt guest boots UEFI'); +unlike($riscv, qr/<(?:pae|acpi|apic)\b/, + 'pae, acpi and apic are x86 features and are left out of a riscv64 guest'); +unlike($riscv, qr/]*\btype="kvm"/, 'a POWER guest stays a kvm domain'); +like(os_type_element($power), qr/\barch="ppc64"/, 'ppc64le hypervisors keep arch ppc64'); +like(os_type_element($power), qr/\bmachine="pseries"/, 'ppc64le hypervisors keep machine pseries'); + +# x86_64 on x86_64 is unchanged: libvirt picks the arch and the machine type. +my $x86 = domain_xml('x86_64', 'x86_64'); +like($x86, qr/]*\btype="kvm"/, 'an x86_64 guest stays a kvm domain'); +unlike(os_type_element($x86), qr/\barch=/, 'an x86_64 guest states no arch'); +unlike(os_type_element($x86), qr/\bmachine=/, 'an x86_64 guest states no machine type'); +like($x86, qr/]*\bbus="usb"/, 'an x86_64 guest keeps the USB tablet'); + +# The disks of a riscv64 guest. The virt machine has no IDE controller, so an ide disk or an +# hd* optical drive makes libvirt refuse the domain. +sub disk_struct { + my ($guest_arch) = @_; + local $KVMArch::node = 'cn1'; + local $KVMArch::confdata = { + vm => { cn1 => [ { host => 'hyp1', storage => '/var/lib/libvirt/images/cn1.img' } ] }, + nodetype => { cn1 => [ { arch => $guest_arch } ] }, + hyp1 => { cpumodel => 'x86_64' }, + }; + my $chatter = ''; + my $disks; + { + open(my $capture, '>', \\$chatter) or die "capture stdout: $!"; + local *STDOUT = $capture; + ($disks) = KVMArch::build_diskstruct(undef); + } + return $disks; +} + +my $riscv_disks = disk_struct('riscv64'); +is($riscv_disks->[0]->{target}->{bus}, 'scsi', 'a riscv64 disk is scsi, not ide'); +like($riscv_disks->[0]->{target}->{dev}, qr/^sd/, 'a riscv64 disk is named sd*'); +is($riscv_disks->[1]->{device}, 'cdrom', 'the guest still gets an optical drive'); +like($riscv_disks->[1]->{target}->{dev}, qr/^sd/, 'a riscv64 optical drive is named sd*, not hd*'); + +my $x86_disks = disk_struct('x86_64'); +is($x86_disks->[0]->{target}->{bus}, 'ide', 'an x86_64 disk keeps the ide default'); +like($x86_disks->[1]->{target}->{dev}, qr/^hd/, 'an x86_64 optical drive keeps the hd* name'); + +done_testing();