diff --git a/docs/configuration.markdown b/docs/configuration.markdown index 3d9ab9c..b1390dd 100644 --- a/docs/configuration.markdown +++ b/docs/configuration.markdown @@ -149,6 +149,14 @@ end {:cpus => "2-3", :memory => "4096"} ] ``` +* `launchsecurity` - Configure Secure Encryption Virtualization for the guest, requires additional components to be configured to work, see [examples](./examples.html#secure-encryption-virtualization). For more information look at [libvirt documentation](https://libvirt.org/kbase/launch_security_sev.html). + ``` + libvirt.launchsecurity :type => 'sev', :cbitpos => 47, :reducedPhysBits => 1, :policy => "0x0003" + ``` +* `memtune` - Configure the memtune settings for the guest, primarily exposed to facilitate enabling Secure Encryption Virtualization. Note that when configuring `hard_limit` that the value is in kB as opposed to `libvirt.memory` which is in Mb. Additionally it must be set to be higher than `libvirt.memory`, see [libvirt documentation](https://libvirt.org/kbase/launch_security_sev.html) for details on why. + ``` + libvirt.memtune :type => "hard_limit", :value => 2500000 # Note here the value in kB (not in Mb) + ``` * `loader` - Sets path to custom UEFI loader. * `kernel` - To launch the guest with a kernel residing on host filesystems. Equivalent to qemu `-kernel`. diff --git a/docs/examples.markdown b/docs/examples.markdown index 00713cd..39731b4 100644 --- a/docs/examples.markdown +++ b/docs/examples.markdown @@ -477,6 +477,66 @@ Vagrant.configure("2") do |config| end ``` +## Secure Encryption Virtualization (SEV) + +Secure Encryption Virtualization is supported by libvirt and by the vagrant-libvirt provider but comes with several requirements. + +This mode has only been tested with q35 types of machines, so you'll need an UEFI boot + +```ruby +Vagrant.configure("2") do |config| + config.vm.provider :libvirt do |libvirt| + libvirt.loader = "/usr/share/OVMF/OVMF_CODE.fd" + libvirt.nvram = "/path/to/ovmf/OVMF_VARS.fd" + libvirt.machine_type = 'pc-q35-focal' + end +end +``` + +Read the libvirt documentaiton to understand what OVMF is and how to use it. + +Next, you'll want to call the following methods: +```ruby +Vagrant.configure("2") do |config| + config.vm.provider :libvirt do |libvirt| + libvirt.launchsecurity :type => 'sev', :cbitpos => 47, :reducedPhysBits => 1, :policy => "0x0003" + libvirt.memtune :type => "hard_limit", :value => 2500000 # Note here the value in kB (not in Mb) + end +end +``` + +Note that the value provided in the memtune `hard_limit` is in Kb by default. It should be higher than the +one given in `libvirt.memory` (which is in Mb, by the way) by some amount (again, check out the [https://libvirt.org/kbase/launch_security_sev.html](documentation)) to understand why. + +It is also necessary to explicitly define the memballoon for it to accept the iommu flag. + +```ruby +Vagrant.configure("2") do |config| + config.vm.provider :libvirt do |libvirt| + libvirt.memballoon_enabled = true + libvirt.memballoon_model = 'virtio' + libvirt.memballoon_pci_bus = '0x07' + libvirt.memballoon_pci_slot = '0x00' + end +end +``` + +And finally, because the iommu flag has to be passed to the networks, you also need to set it explicitly: + +```ruby +Vagrant.configure("2") do |config| + config.vm.provider :libvirt do |libvirt| + # Management network only (the NAT'ed network provided by Vagrant) + libvirt.management_network_driver_iommu = true + end + # Example in defining a bridge + config.vm.network :public_network, :dev => "br0", :bridge => "br0", :mode => "bridge", :type => "bridge", :driver_iommu => true # <== Note here the additional flag +end +``` + +Don't forget that you'll need an UEFI base box. + + ## Libvirt communication channels For certain functionality to be available within a guest, a private diff --git a/lib/vagrant-libvirt/action/create_domain.rb b/lib/vagrant-libvirt/action/create_domain.rb index f97f419..fc102f6 100644 --- a/lib/vagrant-libvirt/action/create_domain.rb +++ b/lib/vagrant-libvirt/action/create_domain.rb @@ -38,6 +38,7 @@ module VagrantPlugins @features_hyperv = config.features_hyperv @clock_offset = config.clock_offset @clock_timers = config.clock_timers + @launchsecurity_data = config.launchsecurity_data @shares = config.shares @cpu_mode = config.cpu_mode @cpu_model = config.cpu_model @@ -52,6 +53,8 @@ module VagrantPlugins @nested = config.nested @memory_size = config.memory.to_i * 1024 @memory_backing = config.memory_backing + @memtunes = config.memtunes + @management_network_mac = config.management_network_mac @domain_volume_cache = config.volume_cache || 'default' @kernel = config.kernel @@ -240,6 +243,10 @@ module VagrantPlugins @memory_backing.each do |backing| env[:ui].info(" -- Memory Backing: #{backing[:name]}: #{backing[:config].map { |k,v| "#{k}='#{v}'"}.join(' ')}") end + + @memtunes.each do |type, options| + env[:ui].info(" -- Memory Tuning: #{type}: #{options[:config].map { |k,v| "#{k}='#{v}'"}.join(' ')}, value: #{options[:value]}") + end unless @shares.nil? env[:ui].info(" -- Shares: #{@shares}") end @@ -311,6 +318,10 @@ module VagrantPlugins env[:ui].info(" -- Disks: #{_disks_print(@disks)}") end + if not @launchsecurity_data.nil? + env[:ui].info(" -- Launch security: #{@launchsecurity_data.map { |k, v| "#{k.to_s}=#{v}" }.join(", ")}") + end + @disks.each do |disk| msg = " -- Disk(#{disk[:device]}): #{disk[:absolute_path]}" msg += ' Shared' if disk[:shareable] diff --git a/lib/vagrant-libvirt/action/create_network_interfaces.rb b/lib/vagrant-libvirt/action/create_network_interfaces.rb index 3b98b24..c4c5ca2 100644 --- a/lib/vagrant-libvirt/action/create_network_interfaces.rb +++ b/lib/vagrant-libvirt/action/create_network_interfaces.rb @@ -76,6 +76,7 @@ module VagrantPlugins @mac = iface_configuration.fetch(:mac, false) @model_type = iface_configuration.fetch(:model_type, @nic_model_type) @driver_name = iface_configuration.fetch(:driver_name, false) + @driver_iommu = iface_configuration.fetch(:driver_iommu, false ) @driver_queues = iface_configuration.fetch(:driver_queues, false) @device_name = iface_configuration.fetch(:iface_name, false) @mtu = iface_configuration.fetch(:mtu, nil) @@ -84,12 +85,15 @@ module VagrantPlugins template_name = 'interface' @type = nil @udp_tunnel = nil + + @logger.debug("Interface configuration: #{iface_configuration}") # Configuration for public interfaces which use the macvtap driver if iface_configuration[:iface_type] == :public_network @device = iface_configuration.fetch(:dev, 'eth0') @mode = iface_configuration.fetch(:mode, 'bridge') @type = iface_configuration.fetch(:type, 'direct') @model_type = iface_configuration.fetch(:model_type, @nic_model_type) + @driver_iommu = iface_configuration.fetch(:driver_iommu, false ) @driver_name = iface_configuration.fetch(:driver_name, false) @driver_queues = iface_configuration.fetch(:driver_queues, false) @portgroup = iface_configuration.fetch(:portgroup, nil) @@ -124,13 +128,14 @@ module VagrantPlugins } @tunnel_type = iface_configuration.fetch(:model_type, @nic_model_type) @driver_name = iface_configuration.fetch(:driver_name, false) + @driver_iommu = iface_configuration.fetch(:driver_iommu, false ) @driver_queues = iface_configuration.fetch(:driver_queues, false) template_name = 'tunnel_interface' @logger.info("Setting up #{@type} tunnel interface using #{@tunnel_ip} port #{@tunnel_port}") end message = "Creating network interface eth#{@iface_number}" - message += " connected to network #{@network_name}." + message += " connected to network #{@network_name} based on template #{template_name}." if @mac @mac = @mac.scan(/(\h{2})/).join(':') message += " Using MAC address: #{@mac}" @@ -141,7 +146,9 @@ module VagrantPlugins # FIXME: all options for network driver should be hash from Vagrantfile driver_options = {} driver_options[:name] = @driver_name if @driver_name + driver_options[:iommu] = @driver_iommu ? "on" : "off" driver_options[:queues] = @driver_queues if @driver_queues + @udp_tunnel ||= {} xml = if template_name == 'interface' or template_name == 'tunnel_interface' @@ -259,12 +266,15 @@ module VagrantPlugins xml.source(source_options) do xml.local(udp_tunnel) if type == 'udp' end + + @logger.debug "Driver options: #{driver_options}" + xml.mac(address: mac) if mac xml.target(dev: target_dev_name(device_name, type, iface_number)) xml.alias(name: "net#{iface_number}") xml.model(type: model_type.to_s) xml.mtu(size: Integer(mtu)) if mtu - xml.driver(driver_options) + xml.driver(**driver_options) xml.address(type: 'pci', bus: pci_bus, slot: pci_slot) if pci_bus and pci_slot end end.to_xml( diff --git a/lib/vagrant-libvirt/action/start_domain.rb b/lib/vagrant-libvirt/action/start_domain.rb index 29dea41..45155c3 100644 --- a/lib/vagrant-libvirt/action/start_domain.rb +++ b/lib/vagrant-libvirt/action/start_domain.rb @@ -194,6 +194,68 @@ module VagrantPlugins end end + # Launch security + launchSecurity = REXML::XPath.first(xml_descr, '/domain/launchSecurity') + unless config.launchsecurity_data.nil? + if launchSecurity.nil? + @logger.debug "Launch security has been added" + launchSecurity = REXML::Element.new('launchSecurity', REXML::XPath.first(xml_descr, '/domain')) + descr_changed = true + end + + if launchSecurity.attributes['type'] != config.launchsecurity_data[:type] + launchSecurity.attributes['type'] = config.launchsecurity_data[:type] + descr_changed = true + end + + [:cbitpos, :policy, :reducedPhysBits].each do |setting| + setting_value = config.launchsecurity_data[setting] + element = REXML::XPath.first(launchSecurity, setting.to_s) + if !setting_value.nil? + if element.nil? + element = launchSecurity.add_element(setting.to_s) + descr_changed = true + end + + if element.text != setting_value + @logger.debug "launchSecurity #{setting.to_s} config changed" + element.text = setting_value + descr_changed = true + end + else + if !element.nil? + launchSecurity.delete_element(setting.to_s) + descr_changed = true + end + end + end + + controllers = REXML::XPath.each( xml_descr, '/domain/devices/controller') + memballoon = REXML::XPath.each( xml_descr, '/domain/devices/memballoon') + [controllers, memballoon].lazy.flat_map(&:lazy).each do |controller| + driver_node = REXML::XPath.first(controller, 'driver') + driver_node = controller.add_element('driver') if driver_node.nil? + descr_changed = true if driver_node.attributes['iommu'] != 'on' + driver_node.attributes['iommu'] = 'on' + end + else + unless launchSecurity.nil? + @logger.debug "Launch security to be deleted" + + descr_changed = true + + launchSecurity.parent.delete_element(launchSecurity) + end + + REXML::XPath.each( xml_descr, '/domain/devices/controller') do | controller | + driver_node = REXML::XPath.first(controller, 'driver') + if !driver_node.nil? + descr_changed = true if driver_node.attributes['iommu'] + driver_node.attributes.delete('iommu') + end + end + end + # Graphics graphics = REXML::XPath.first(xml_descr, '/domain/devices/graphics') if config.graphics_type != 'none' diff --git a/lib/vagrant-libvirt/config.rb b/lib/vagrant-libvirt/config.rb index 94daf0c..c311828 100644 --- a/lib/vagrant-libvirt/config.rb +++ b/lib/vagrant-libvirt/config.rb @@ -67,6 +67,7 @@ module VagrantPlugins attr_accessor :management_network_domain attr_accessor :management_network_mtu attr_accessor :management_network_keep + attr_accessor :management_network_driver_iommu # System connection information attr_accessor :system_uri @@ -81,6 +82,7 @@ module VagrantPlugins attr_accessor :memory attr_accessor :nodeset attr_accessor :memory_backing + attr_accessor :memtunes attr_accessor :channel attr_accessor :cpus attr_accessor :cpuset @@ -95,6 +97,7 @@ module VagrantPlugins attr_accessor :features_hyperv attr_accessor :clock_offset attr_accessor :clock_timers + attr_accessor :launchsecurity_data attr_accessor :numa_nodes attr_accessor :loader attr_accessor :nvram @@ -243,6 +246,7 @@ module VagrantPlugins @management_network_domain = UNSET_VALUE @management_network_mtu = UNSET_VALUE @management_network_keep = UNSET_VALUE + @management_network_driver_iommu = UNSET_VALUE # System connection information @system_uri = UNSET_VALUE @@ -254,6 +258,7 @@ module VagrantPlugins @memory = UNSET_VALUE @nodeset = UNSET_VALUE @memory_backing = UNSET_VALUE + @memtunes = {} @cpus = UNSET_VALUE @cpuset = UNSET_VALUE @cpu_mode = UNSET_VALUE @@ -267,6 +272,7 @@ module VagrantPlugins @features_hyperv = UNSET_VALUE @clock_offset = UNSET_VALUE @clock_timers = [] + @launchsecurity_data = UNSET_VALUE @numa_nodes = UNSET_VALUE @loader = UNSET_VALUE @nvram = UNSET_VALUE @@ -528,6 +534,37 @@ module VagrantPlugins config: config) end + def memtune(config={}) + if config[:type].nil? + raise "Missing memtune type" + end + + unless ['hard_limit', 'soft_limit', 'swap_hard_limit'].include? config[:type] + raise "Memtune type '#{config[:type]}' not allowed (hard_limit, soft_limit, swap_hard_limit are allowed)" + end + + if config[:value].nil? + raise "Missing memtune value" + end + + opts = config[:options] || {} + opts[:unit] = opts[:unit] || "KiB" + + @memtunes[config[:type]] = { value: config[:value], config: opts } + end + + def launchsecurity(options = {}) + if options.fetch(:type) != 'sev' + raise "Launch security type only supports SEV. Explicitly set 'sev' as a type" + end + + @launchsecurity_data = {} + @launchsecurity_data[:type] = options[:type] + @launchsecurity_data[:cbitpos] = options[:cbitpos] || 47 + @launchsecurity_data[:reducedPhysBits] = options[:reducedPhysBits] || 1 + @launchsecurity_data[:policy] = options[:policy] || "0x0003" + end + def input(options = {}) if options[:type].nil? || options[:bus].nil? raise 'Input type AND bus must be specified' @@ -916,6 +953,7 @@ module VagrantPlugins @management_network_domain = nil if @management_network_domain == UNSET_VALUE @management_network_mtu = nil if @management_network_mtu == UNSET_VALUE @management_network_keep = false if @management_network_keep == UNSET_VALUE + @management_network_driver_iommu = false if @management_network_driver_iommu == UNSET_VALUE # Domain specific settings. @title = '' if @title == UNSET_VALUE @@ -954,6 +992,7 @@ module VagrantPlugins @features_hyperv = [] if @features_hyperv == UNSET_VALUE @clock_offset = 'utc' if @clock_offset == UNSET_VALUE @clock_timers = [] if @clock_timers == UNSET_VALUE + @launchsecurity_data = nil if @launchsecurity_data == UNSET_VALUE @numa_nodes = @numa_nodes == UNSET_VALUE ? nil : _generate_numa @loader = nil if @loader == UNSET_VALUE @nvram = nil if @nvram == UNSET_VALUE @@ -1224,6 +1263,8 @@ module VagrantPlugins c += other.floppies result.floppies = c + result.memtunes = memtunes.merge(other.memtunes) + result.disk_driver_opts = disk_driver_opts.merge(other.disk_driver_opts) result.inputs = inputs != UNSET_VALUE ? inputs.dup + (other.inputs != UNSET_VALUE ? other.inputs : []) : other.inputs diff --git a/lib/vagrant-libvirt/templates/domain.xml.erb b/lib/vagrant-libvirt/templates/domain.xml.erb index 7064a10..53e48b3 100644 --- a/lib/vagrant-libvirt/templates/domain.xml.erb +++ b/lib/vagrant-libvirt/templates/domain.xml.erb @@ -45,6 +45,13 @@ <<%= backing[:name] %> <%= backing[:config].map { |k,v| "#{k}='#{v}'"}.join(' ') %>/> <%- end -%> +<%- end -%> +<%- unless @memtunes.empty? -%> + + <%- @memtunes.each do |name, options| -%> + <<%= name %> <%= options[:config].map { |k,v| "#{k}='#{v}'"}.join(' ') %>><%= options[:value] %>> + <%- end -%> + <%- end%> <%- if !@cpu_affinity.empty? || @shares -%> @@ -229,7 +236,11 @@ <%- end -%> <%- @inputs.each do |input| -%> - + + <%- unless @launchsecurity_data.nil? -%> + + <%- end -%> + <%- end -%> <%- if !@sound_type.nil? -%> <%# Sound device-%> @@ -265,6 +276,9 @@ <%- if @rng[:model] == "random"%> /dev/random + <%- unless @launchsecurity_data.nil? -%> + + <%- end -%> <%- end -%> <%- @@ -338,18 +352,32 @@ <%- end -%> <%- if not @usbctl_dev.empty? -%> <%# USB Controller -%> - /> + > + <%- unless @launchsecurity_data.nil? -%> + + <%- end -%> + <%- end -%> <%- unless @memballoon_enabled.nil? -%> <%- if @memballoon_enabled -%>
+ <%- unless @launchsecurity_data.nil? -%> + + <%- end -%> <%- else -%> <%- end -%> <%- end -%> +<%- unless @launchsecurity_data.nil? -%> + + <%= @launchsecurity_data[:cbitpos] %> + <%= @launchsecurity_data[:reducedPhysBits] %> + <%= @launchsecurity_data[:policy] %> + +<%- end -%> <%- if not @qemu_args.empty? or not @qemu_env.empty? -%> <%- @qemu_args.each do |arg| -%> diff --git a/lib/vagrant-libvirt/templates/public_interface.xml.erb b/lib/vagrant-libvirt/templates/public_interface.xml.erb index 83bc528..cf98a09 100644 --- a/lib/vagrant-libvirt/templates/public_interface.xml.erb +++ b/lib/vagrant-libvirt/templates/public_interface.xml.erb @@ -12,12 +12,15 @@ <% end %> <% if @driver_name and @driver_queues %> - + iommu="on" <% end %> name='<%=@driver_name%>' queues='<%=@driver_queues%>'/> <% elsif @driver_queues %> - + iommu="on" <% end %> queues='<%=@driver_queues%>'/> <% elsif @driver_name %> - + iommu="on" <% end %> name='<%=@driver_name%>'/> + <% elsif @driver_iommu %> + <% end %> + <% if @ovs %> <% if @ovs_interfaceid %> diff --git a/lib/vagrant-libvirt/util/network_util.rb b/lib/vagrant-libvirt/util/network_util.rb index 2e7cc60..b29182e 100644 --- a/lib/vagrant-libvirt/util/network_util.rb +++ b/lib/vagrant-libvirt/util/network_util.rb @@ -33,6 +33,7 @@ module VagrantPlugins management_network_domain = env[:machine].provider_config.management_network_domain management_network_mtu = env[:machine].provider_config.management_network_mtu management_network_keep = env[:machine].provider_config.management_network_keep + management_network_driver_iommu = env[:machine].provider_config.management_network_driver_iommu logger.info "Using #{management_network_name} at #{management_network_address} as the management network #{management_network_mode} is the mode" begin @@ -74,7 +75,7 @@ module VagrantPlugins } end - + management_network_options[:driver_iommu] = management_network_driver_iommu unless management_network_mac.nil? management_network_options[:mac] = management_network_mac diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1bcf93b..998200f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -73,6 +73,10 @@ RSpec.configure do |config| # don't run acceptance tests by default config.filter_run_excluding :acceptance => true + + config.expect_with :rspec do |c| + c.max_formatted_output_length = 2000 if c.respond_to?("max_formatted_output_length=") + end end begin diff --git a/spec/unit/action/create_domain_spec.rb b/spec/unit/action/create_domain_spec.rb index ef4389c..b2b6e1c 100644 --- a/spec/unit/action/create_domain_spec.rb +++ b/spec/unit/action/create_domain_spec.rb @@ -185,6 +185,38 @@ describe VagrantPlugins::ProviderLibvirt::Action::CreateDomain do end end + context 'launchSecurity' do + let(:vagrantfile_providerconfig) do + <<-EOF + libvirt.launchsecurity :type => 'sev', :cbitpos => 47, :reducedPhysBits => 1, :policy => "0x0003" + EOF + end + + it 'should emit the settings to the ui' do + expect(ui).to receive(:info).with(/ -- Launch security: type=sev, cbitpos=47, reducedPhysBits=1, policy=0x0003/) + expect(servers).to receive(:create).and_return(machine) + + expect(subject.call(env)).to be_nil + end + end + + context 'memtunes' do + let(:vagrantfile_providerconfig) do + <<-EOF + libvirt.memtune :type => 'hard_limit', :value => 250000 + libvirt.memtune :type => 'soft_limit', :value => 200000 + EOF + end + + it 'should emit the settings to the ui' do + expect(ui).to receive(:info).with(/ -- Memory Tuning: hard_limit: unit='KiB', value: 250000/) + expect(ui).to receive(:info).with(/ -- Memory Tuning: soft_limit: unit='KiB', value: 200000/) + expect(servers).to receive(:create).and_return(machine) + + expect(subject.call(env)).to be_nil + end + end + context 'sysinfo' do let(:domain_xml_file) { 'sysinfo.xml' } let(:vagrantfile_providerconfig) do diff --git a/spec/unit/action/create_domain_spec/additional_disks_domain.xml b/spec/unit/action/create_domain_spec/additional_disks_domain.xml index 0f1c949..95c04a0 100644 --- a/spec/unit/action/create_domain_spec/additional_disks_domain.xml +++ b/spec/unit/action/create_domain_spec/additional_disks_domain.xml @@ -41,7 +41,8 @@ - + +