From 63d265d9ca18fda182a0d500ebc585aeb53dcdea Mon Sep 17 00:00:00 2001 From: Nils Ballmann <45712604+nsballmann@users.noreply.github.com> Date: Tue, 16 Aug 2022 18:44:11 +0200 Subject: [PATCH] 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 Co-authored-by: Darragh Bailey --- docs/configuration.markdown | 5 + docs/examples.markdown | 38 ++++++++ lib/vagrant-libvirt/action/create_domain.rb | 32 +++++++ lib/vagrant-libvirt/config.rb | 59 ++++++++++++ lib/vagrant-libvirt/templates/domain.xml.erb | 20 ++++ spec/unit/action/create_domain_spec.rb | 64 +++++++++++++ .../action/create_domain_spec/sysinfo.xml | 66 +++++++++++++ .../sysinfo_only_required.xml | 49 ++++++++++ spec/unit/config_spec.rb | 92 +++++++++++++++++-- spec/unit/templates/domain_all_settings.xml | 9 ++ spec/unit/templates/domain_spec.rb | 16 ++++ 11 files changed, 444 insertions(+), 6 deletions(-) create mode 100644 spec/unit/action/create_domain_spec/sysinfo.xml create mode 100644 spec/unit/action/create_domain_spec/sysinfo_only_required.xml diff --git a/docs/configuration.markdown b/docs/configuration.markdown index 2065604..4a0bb38 100644 --- a/docs/configuration.markdown +++ b/docs/configuration.markdown @@ -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. diff --git a/docs/examples.markdown b/docs/examples.markdown index 0e24368..393a31a 100644 --- a/docs/examples.markdown +++ b/docs/examples.markdown @@ -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, diff --git a/lib/vagrant-libvirt/action/create_domain.rb b/lib/vagrant-libvirt/action/create_domain.rb index a0158bd..7fa3a5a 100644 --- a/lib/vagrant-libvirt/action/create_domain.rb +++ b/lib/vagrant-libvirt/action/create_domain.rb @@ -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}") diff --git a/lib/vagrant-libvirt/config.rb b/lib/vagrant-libvirt/config.rb index 6fa4c50..83e63ee 100644 --- a/lib/vagrant-libvirt/config.rb +++ b/lib/vagrant-libvirt/config.rb @@ -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 diff --git a/lib/vagrant-libvirt/templates/domain.xml.erb b/lib/vagrant-libvirt/templates/domain.xml.erb index 8def8e6..3e98c71 100644 --- a/lib/vagrant-libvirt/templates/domain.xml.erb +++ b/lib/vagrant-libvirt/templates/domain.xml.erb @@ -81,8 +81,28 @@ <%= @cmd_line %> <%- if @dtb -%> <%= @dtb %> +<% end -%> +<%- unless @sysinfo.empty? -%> + <% end -%> +<%- unless @sysinfo.empty? -%> + + <%- @sysinfo.each_pair do |block, values| -%> + <<%= @sysinfo_blocks[block.to_s][:xml] %>> + <%- if values.respond_to?(:each_pair) -%> + <%- values.each do |name, value| -%> + <%= value %> + <% end -%> + <%- else -%> + <%- values.each do |value| -%> + <%= value %> + <% end -%> + <% end -%> + > + <% end -%> + +<% end -%> <%- @features.each do |feature| -%> <<%= feature %>/> diff --git a/spec/unit/action/create_domain_spec.rb b/spec/unit/action/create_domain_spec.rb index a47e350..1733a14 100644 --- a/spec/unit/action/create_domain_spec.rb +++ b/spec/unit/action/create_domain_spec.rb @@ -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 diff --git a/spec/unit/action/create_domain_spec/sysinfo.xml b/spec/unit/action/create_domain_spec/sysinfo.xml new file mode 100644 index 0000000..1137dea --- /dev/null +++ b/spec/unit/action/create_domain_spec/sysinfo.xml @@ -0,0 +1,66 @@ + + vagrant-test_default + + Source: /rootpath/Vagrantfile + + 524288 + 1 + + + + + hvm + + + + + + + + Test Vendor + + + Test Manufacturer + 0.1.0 + + + Test Manufacturer + + + Test Manufacturer + AABBCCDDEE + + + app1: string1 + app1: string2 + app2: string1 + app2: string2 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/unit/action/create_domain_spec/sysinfo_only_required.xml b/spec/unit/action/create_domain_spec/sysinfo_only_required.xml new file mode 100644 index 0000000..f9868c4 --- /dev/null +++ b/spec/unit/action/create_domain_spec/sysinfo_only_required.xml @@ -0,0 +1,49 @@ + + vagrant-test_default + + Source: /rootpath/Vagrantfile + + 524288 + 1 + + + + + hvm + + + + + + + + Test Vendor + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/unit/config_spec.rb b/spec/unit/config_spec.rb index d91b513..6727782 100644 --- a/spec/unit/config_spec.rb +++ b/spec/unit/config_spec.rb @@ -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 diff --git a/spec/unit/templates/domain_all_settings.xml b/spec/unit/templates/domain_all_settings.xml index 03a77ee..f3c058e 100644 --- a/spec/unit/templates/domain_all_settings.xml +++ b/spec/unit/templates/domain_all_settings.xml @@ -23,7 +23,16 @@ + + + + AAAAAAAA + + + AAAAAAAA + + diff --git a/spec/unit/templates/domain_spec.rb b/spec/unit/templates/domain_spec.rb index 1c11e61..1c8ee8a 100644 --- a/spec/unit/templates/domain_spec.rb +++ b/spec/unit/templates/domain_spec.rb @@ -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')