add sysinfo support (#1500)

For testing certain scenarios with vagrant-libvirt, need in the guest system a
value for the systems serial number in the DMI/SMBIOS system information.
The domain https://libvirt.org/formatdomain.html#smbios-system-information
format of libvirt allows to specify those values.

While adding `-smbios type=1,serial=$serial_value` to the `qemuargs` parameter
of the libvirt provider is already able to achieve this, a dedicated provider config
value adds native support from the `Vagrantfile` layering system. For example,
in the .box included Vagrantfile a random serial number can be enforced by
adding the following:

require 'securerandom'
Vagrant.configure("2") do |config|
  config.vm.provider :libvirt do |libvirt|
    libvirt.dmi_system_serial = SecureRandom.alphanumeric(8).upcase
  end
end

Then in an instance specific Vagrantfile this value can be overwritten by adding:

Vagrant.configure("2") do |config|
  config.vm.provider :libvirt do |libvirt|
    libvirt.dmi_system_serial = "ABCDEFGH"
  end
end

Co-authored-by: Nils Ballmann <nils.ballmann.ext@siemens.com>
Co-authored-by: Darragh Bailey <daragh.bailey@gmail.com>
This commit is contained in:
Nils Ballmann 2022-08-16 18:44:11 +02:00 committed by GitHub
parent 77e53a2f53
commit 63d265d9ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 444 additions and 6 deletions

View File

@ -210,6 +210,11 @@ end
* `tpm_type` - The type of TPM device to which you are connecting.
* `tpm_path` - The path to the TPM device on the host system.
* `tpm_version` - The TPM version to use.
* `sysinfo` - The [SMBIOS System Information](https://libvirt.org/formatdomain.html#smbios-system-information) to use.
This is a hash with key names aligning with the different section XML tags of
bios, system, base board, chassis, and oem strings. Nested hashes then use
entry attribute names as the keys for the values to assign, except for oem strings
which is a simple array of strings.
* `dtb` - The device tree blob file, mostly used for non-x86 platforms. In case
the device tree isn't added in-line to the kernel, it can be manually
specified here.

View File

@ -360,6 +360,44 @@ Vagrant.configure("2") do |config|
end
```
## SMBIOS System Information
Libvirt allows to specify
[SMBIOS System Information](https://libvirt.org/formatdomain.html#smbios-system-information)
like a base board or chassis manufacturer or a system serial number.
```ruby
Vagrant.configure("2") do |config|
config.vm.provider :libvirt do |libvirt|
libvirt.sysinfo = {
'bios': {
'vendor': 'Test Vendor',
'version': '0.1.2',
},
'system': {
'manufacturer': 'Test Manufacturer',
'version': '0.1.0',
'serial': '',
},
'base board': {
'manufacturer': 'Test Manufacturer',
'version': '1.2',
},
'chassis': {
'manufacturer': 'Test Manufacturer',
'serial': 'AABBCCDDEE',
},
'oem strings': [
'app1: string1',
'app1: string2',
'app2: string1',
'app2: string2',
],
}
end
end
```
## Memory balloon
The configuration of the memory balloon device can be overridden. By default,

View File

@ -74,6 +74,22 @@ module VagrantPlugins
@tpm_path = config.tpm_path
@tpm_version = config.tpm_version
@sysinfo = config.sysinfo.dup
@sysinfo.each do |section, _v|
if @sysinfo[section].respond_to?(:each_pair)
@sysinfo[section].delete_if { |_k, v| v.to_s.empty? }
else
@sysinfo[section].reject! { |e| e.to_s.empty? }
end
end.reject! { |_k, v| v.empty? }
@sysinfo_blocks = {
'bios' => {:ui => "BIOS", :xml => "bios"},
'system' => {:ui => "System", :xml => "system"},
'base board' => {:ui => "Base Board", :xml => "baseBoard"},
'chassis' => {:ui => "Chassis", :xml => "chassis"},
'oem strings' => {:ui => "OEM Strings", :xml => "oemStrings"},
}
# Boot order
@boot_order = config.boot_order
@ -256,6 +272,22 @@ module VagrantPlugins
env[:ui].info(" -- TPM Path: #{@tpm_path}")
end
unless @sysinfo.empty?
env[:ui].info(" -- Sysinfo:")
@sysinfo.each_pair do |block, values|
env[:ui].info(" -- #{@sysinfo_blocks[block.to_s][:ui]}:")
if values.respond_to?(:each_pair)
values.each_pair do |name, value|
env[:ui].info(" -> #{name}: #{value}")
end
else
values.each do |value|
env[:ui].info(" -> #{value}")
end
end
end
end
if @memballoon_enabled
env[:ui].info(" -- Memballoon model: #{@memballoon_model}")
env[:ui].info(" -- Memballoon bus: #{@memballoon_pci_bus}")

View File

@ -129,6 +129,9 @@ module VagrantPlugins
attr_accessor :tpm_path
attr_accessor :tpm_version
# Configure sysinfo values
attr_accessor :sysinfo
# Configure the memballoon
attr_accessor :memballoon_enabled
attr_accessor :memballoon_model
@ -285,6 +288,8 @@ module VagrantPlugins
@tpm_path = UNSET_VALUE
@tpm_version = UNSET_VALUE
@sysinfo = UNSET_VALUE
@memballoon_enabled = UNSET_VALUE
@memballoon_model = UNSET_VALUE
@memballoon_pci_bus = UNSET_VALUE
@ -917,6 +922,8 @@ module VagrantPlugins
@nic_adapter_count = 8 if @nic_adapter_count == UNSET_VALUE
@emulator_path = nil if @emulator_path == UNSET_VALUE
@sysinfo = {} if @sysinfo == UNSET_VALUE
# Boot order
@boot_order = [] if @boot_order == UNSET_VALUE
@ -1074,6 +1081,8 @@ module VagrantPlugins
end
end
errors = validate_sysinfo(machine, errors)
{ 'Libvirt Provider' => errors }
end
@ -1089,6 +1098,10 @@ module VagrantPlugins
result.disk_driver_opts = disk_driver_opts.merge(other.disk_driver_opts)
c = sysinfo == UNSET_VALUE ? {} : sysinfo.dup
c.merge!(other.sysinfo) { |_k, x, y| x.respond_to?(:each_pair) ? x.merge(y) : x + y } if other.sysinfo != UNSET_VALUE
result.sysinfo = c
c = clock_timers.dup
c += other.clock_timers
result.clock_timers = c
@ -1206,6 +1219,52 @@ module VagrantPlugins
end
end
end
def validate_sysinfo(machine, errors)
valid_sysinfo = {
'bios' => %w[vendor version date release],
'system' => %w[manufacturer product version serial uuid sku family],
'base board' => %w[manufacturer product version serial asset location],
'chassis' => %w[manufacturer version serial asset sku],
'oem strings' => nil,
}
machine.provider_config.sysinfo.each_pair do |block_name, entries|
block_name = block_name.to_s
unless valid_sysinfo.key?(block_name)
errors << "invalid sysinfo element '#{block_name}'; smbios sysinfo elements supported: #{valid_sysinfo.keys.join(', ')}"
next
end
if valid_sysinfo[block_name].nil?
# assume simple array of text entries
entries.each do |entry|
if entry.respond_to?(:to_str)
if entry.to_s.empty?
machine.ui.warn("Libvirt Provider: 'sysinfo.#{block_name}' contains an empty or nil entry and will be discarded")
end
else
errors << "sysinfo.#{block_name} expects entries to be stringy, got #{entry.class} containing '#{entry}'"
end
end
else
entries.each_pair do |entry_name, entry_text|
entry_name = entry_name.to_s
unless valid_sysinfo[block_name].include?(entry_name)
errors << "'sysinfo.#{block_name}' does not support entry name '#{entry_name}'; entries supported: #{valid_sysinfo[block_name].join(', ')}"
next
end
# this allows removal of entries specified by other Vagrantfile's in the hierarchy
if entry_text.to_s.empty?
machine.ui.warn("Libvirt Provider: sysinfo.#{block_name}.#{entry_name} is nil or empty and therefore has no effect.")
end
end
end
end
errors
end
end
end
end

View File

@ -81,8 +81,28 @@
<cmdline><%= @cmd_line %></cmdline>
<%- if @dtb -%>
<dtb><%= @dtb %></dtb>
<% end -%>
<%- unless @sysinfo.empty? -%>
<smbios mode='sysinfo'/>
<% end -%>
</os>
<%- unless @sysinfo.empty? -%>
<sysinfo type='smbios'>
<%- @sysinfo.each_pair do |block, values| -%>
<<%= @sysinfo_blocks[block.to_s][:xml] %>>
<%- if values.respond_to?(:each_pair) -%>
<%- values.each do |name, value| -%>
<entry name='<%= name %>'><%= value %></entry>
<% end -%>
<%- else -%>
<%- values.each do |value| -%>
<entry><%= value %></entry>
<% end -%>
<% end -%>
</<%= @sysinfo_blocks[block.to_s][:xml] %>>
<% end -%>
</sysinfo>
<% end -%>
<features>
<%- @features.each do |feature| -%>
<<%= feature %>/>

View File

@ -170,6 +170,70 @@ describe VagrantPlugins::ProviderLibvirt::Action::CreateDomain do
expect(subject.call(env)).to be_nil
end
end
context 'sysinfo' do
let(:domain_xml_file) { 'sysinfo.xml' }
let(:vagrantfile_providerconfig) do
<<-EOF
libvirt.sysinfo = {
'bios': {
'vendor': 'Test Vendor',
'version': '',
},
'system': {
'manufacturer': 'Test Manufacturer',
'version': '0.1.0',
'serial': '',
},
'base board': {
'manufacturer': 'Test Manufacturer',
'version': '',
},
'chassis': {
'manufacturer': 'Test Manufacturer',
'serial': 'AABBCCDDEE',
'asset': '',
},
'oem strings': [
'app1: string1',
'app1: string2',
'app2: string1',
'app2: string2',
'',
'',
],
}
EOF
end
it 'should populate sysinfo as expected' do
expect(servers).to receive(:create).with(xml: domain_xml).and_return(machine)
expect(subject.call(env)).to be_nil
end
context 'with block of empty entries' do
let(:domain_xml_file) { 'sysinfo_only_required.xml' }
let(:vagrantfile_providerconfig) do
<<-EOF
libvirt.sysinfo = {
'bios': {
'vendor': 'Test Vendor',
},
'system': {
'serial': '',
},
}
EOF
end
it 'should skip outputting the surrounding tags' do
expect(servers).to receive(:create).with(xml: domain_xml).and_return(machine)
expect(subject.call(env)).to be_nil
end
end
end
end
context 'connection => qemu:///session' do

View File

@ -0,0 +1,66 @@
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>vagrant-test_default</name>
<title></title>
<description>Source: /rootpath/Vagrantfile</description>
<uuid></uuid>
<memory>524288</memory>
<vcpu>1</vcpu>
<cpu mode='host-model'>
<model fallback='allow'></model>
</cpu>
<os>
<type>hvm</type>
<kernel></kernel>
<initrd></initrd>
<cmdline></cmdline>
<smbios mode='sysinfo'/>
</os>
<sysinfo type='smbios'>
<bios>
<entry name='vendor'>Test Vendor</entry>
</bios>
<system>
<entry name='manufacturer'>Test Manufacturer</entry>
<entry name='version'>0.1.0</entry>
</system>
<baseBoard>
<entry name='manufacturer'>Test Manufacturer</entry>
</baseBoard>
<chassis>
<entry name='manufacturer'>Test Manufacturer</entry>
<entry name='serial'>AABBCCDDEE</entry>
</chassis>
<oemStrings>
<entry>app1: string1</entry>
<entry>app1: string2</entry>
<entry>app2: string1</entry>
<entry>app2: string2</entry>
</oemStrings>
</sysinfo>
<features>
<acpi/>
<apic/>
<pae/>
</features>
<clock offset='utc'>
</clock>
<devices>
<disk type='file' device='disk'>
<alias name='ua-box-volume-0'/>
<driver name='qemu' type='qcow2' cache='default'/>
<source file='/var/lib/libvirt/images/vagrant-test_default.img'/>
<target dev='vda' bus='virtio'/>
</disk>
<serial type='pty'>
<target port='0'/>
</serial>
<console type='pty'>
<target port='0'/>
</console>
<input type='mouse' bus='ps2'/>
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1' keymap='en-us'/>
<video>
<model type='cirrus' vram='16384' heads='1'/>
</video>
</devices>
</domain>

View File

@ -0,0 +1,49 @@
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>vagrant-test_default</name>
<title></title>
<description>Source: /rootpath/Vagrantfile</description>
<uuid></uuid>
<memory>524288</memory>
<vcpu>1</vcpu>
<cpu mode='host-model'>
<model fallback='allow'></model>
</cpu>
<os>
<type>hvm</type>
<kernel></kernel>
<initrd></initrd>
<cmdline></cmdline>
<smbios mode='sysinfo'/>
</os>
<sysinfo type='smbios'>
<bios>
<entry name='vendor'>Test Vendor</entry>
</bios>
</sysinfo>
<features>
<acpi/>
<apic/>
<pae/>
</features>
<clock offset='utc'>
</clock>
<devices>
<disk type='file' device='disk'>
<alias name='ua-box-volume-0'/>
<driver name='qemu' type='qcow2' cache='default'/>
<source file='/var/lib/libvirt/images/vagrant-test_default.img'/>
<target dev='vda' bus='virtio'/>
</disk>
<serial type='pty'>
<target port='0'/>
</serial>
<console type='pty'>
<target port='0'/>
</console>
<input type='mouse' bus='ps2'/>
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1' keymap='en-us'/>
<video>
<model type='cirrus' vram='16384' heads='1'/>
</video>
</devices>
</domain>

View File

@ -577,24 +577,28 @@ describe VagrantPlugins::ProviderLibvirt::Config do
def assert_invalid
subject.finalize!
errors = subject.validate(machine)
raise "No errors: #{errors.inspect}" if errors.values.all?(&:empty?)
errors = subject.validate(machine).values.first
expect(errors).to_not be_empty
errors
end
def assert_valid
subject.finalize!
errors = subject.validate(machine)
raise "Errors: #{errors.inspect}" unless errors.values.all?(&:empty?)
errors = subject.validate(machine).values.first
expect(errors).to be_empty
end
describe '#validate' do
before do
allow(machine).to receive(:provider_config).and_return(subject)
allow(machine).to receive(:ui).and_return(ui)
end
it 'is valid with defaults' do
assert_valid
end
context 'with disks defined' do
before { expect(machine).to receive(:provider_config).and_return(subject).at_least(:once) }
it 'is valid if relative path used for disk' do
subject.storage :file, path: '../path/to/file.qcow2'
assert_valid
@ -709,6 +713,60 @@ describe VagrantPlugins::ProviderLibvirt::Config do
assert_valid
end
end
context 'with sysinfo defined' do
context 'when invalid block name provided' do
it 'should be invalid' do
subject.sysinfo = {'bad bios': {'vendor': 'some vendor'}}
errors = assert_invalid
expect(errors).to include(match(/invalid sysinfo element 'bad bios';/))
end
end
context 'when invalid element name provided' do
it 'should be invalid' do
subject.sysinfo = {'bios': {'bad vendor': 'some vendor'}}
errors = assert_invalid
expect(errors).to include(match(/'sysinfo.bios' does not support entry name 'bad vendor'/))
end
end
context 'when empty element value provided' do
it 'should succeed with a warning' do
expect(ui).to receive(:warn).with(/Libvirt Provider: sysinfo.bios.vendor is nil or empty/)
subject.sysinfo = {'bios': {'vendor': ''}}
assert_valid
end
end
context 'when handling "oem strings"' do
it 'should succeed' do
subject.sysinfo = {'oem strings': ['string 1']}
assert_valid
end
context 'when empty entries' do
it 'should succeed with a warning' do
expect(ui).to receive(:warn).with(/Libvirt Provider: 'sysinfo.oem strings' contains an empty/)
subject.sysinfo = {'oem strings': ['']}
assert_valid
end
end
context 'when non string passed' do
it 'should be invalid' do
subject.sysinfo = {'oem strings': [true]}
assert_invalid
end
end
end
end
end
describe '#merge' do
@ -772,5 +830,27 @@ describe VagrantPlugins::ProviderLibvirt::Config do
include(name: 'hpet'))
end
end
context 'sysinfo' do
it 'should merge' do
one.sysinfo = {
'bios' => {'vendor': 'Some Vendor'},
'system' => {'manufacturer': 'some manufacturer'},
'oem strings' => ['string 1'],
}
two.sysinfo = {
'bios' => {'vendor': 'Another Vendor'},
'system' => {'serial': 'AABBCCDDEE'},
'oem strings' => ['string 2'],
}
subject.finalize!
expect(subject.sysinfo).to eq(
'bios' => {'vendor': 'Another Vendor'},
'system' => {'manufacturer': 'some manufacturer', 'serial': 'AABBCCDDEE'},
'oem strings' => ['string 1', 'string 2'],
)
end
end
end
end

View File

@ -23,7 +23,16 @@
<kernel></kernel>
<initrd></initrd>
<cmdline></cmdline>
<smbios mode='sysinfo'/>
</os>
<sysinfo type='smbios'>
<system>
<entry name='serial'>AAAAAAAA</entry>
</system>
<oemStrings>
<entry>AAAAAAAA</entry>
</oemStrings>
</sysinfo>
<features>
<acpi/>
<apic/>

View File

@ -16,6 +16,13 @@ describe 'templates/domain' do
def initialize
super
@domain_volumes = []
@sysinfo_blocks = {
'bios' => {:section => "BIOS", :xml => "bios"},
'system' => {:section => "System", :xml => "system"},
'base board' => {:section => "Base Board", :xml => "baseBoard"},
'chassis' => {:section => "Chassis", :xml => "chassis"},
'oem strings' => {:section => "OEM Strings", :xml => "oemStrings"},
}
end
def finalize!
@ -100,6 +107,15 @@ describe 'templates/domain' do
domain.smartcard(mode: 'passthrough')
domain.tpm_path = '/dev/tpm0'
domain.sysinfo = {
'system' => {
'serial' => 'AAAAAAAA',
},
'oem strings' => [
'AAAAAAAA',
],
}
domain.qemuargs(value: '-device')
domain.qemuargs(value: 'dummy-device')