Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #37823 - Add Secure Boot and Virtual TPM support for VMware #10324

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions app/models/compute_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,68 @@ def normalize_vm_attrs(vm_attrs)
vm_attrs
end

# Returns a hash of firmware type identifiers and their corresponding labels for use in the VM creation form.
#
# @return [Hash<String, String>] a hash mapping firmware type identifiers to labels.
def firmware_types
{
"automatic" => N_("Automatic"),
"bios" => N_("BIOS"),
"uefi" => N_("UEFI"),
"uefi_secure_boot" => N_("UEFI Secure Boot"),
}.freeze
end

# Converts the firmware type from a VM object to the Foreman-compatible format.
#
# @param firmware [String] The firmware type from the VM object.
# @param secure_boot [Boolean] Indicates if secure boot is enabled for the VM.
# @return [String] The converted firmware type.
def firmware_type(firmware, secure_boot)
if firmware == 'efi'
secure_boot ? 'uefi_secure_boot' : 'uefi' # Adjust for secure boot
else
firmware
end
end

# Converts a firmware type from Foreman format to a CR-compatible format.
# If no specific type is provided, defaults to 'bios'.
#
# @param firmware_type [String] The firmware type in Foreman format.
# @return [String] The converted firmware type.
def normalize_firmware_type(firmware_type)
case firmware_type
when 'uefi', 'uefi_secure_boot'
'efi'
else
'bios'
end
end

# Resolves the firmware setting when it is 'automatic' based on the provided firmware_type, or defaults to 'bios'.
#
# @param firmware [String] The current firmware setting.
# @param firmware_type [String] The type of firmware to be used if firmware is 'automatic'.
# @return [String] the resolved firmware.
def resolve_automatic_firmware(firmware, firmware_type)
return firmware unless firmware == 'automatic'
firmware_type.presence || 'bios'
end

# Processes firmware attributes to configure firmware and secure boot settings.
#
# @param firmware [String] The firmware setting to be processed.
# @param firmware_type [String] The firmware type based on the provided PXE Loader.
# @return [Hash] A hash containing the processed firmware attributes.
def process_firmware_attributes(firmware, firmware_type)
firmware = resolve_automatic_firmware(firmware, firmware_type)

attrs = generate_secure_boot_settings(firmware)
attrs[:firmware] = normalize_firmware_type(firmware)
attrs
end

protected

def memory_gb_to_bytes(memory_size)
Expand Down
22 changes: 21 additions & 1 deletion app/models/compute_resources/foreman/model/libvirt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ def new_vm(attr = { })
opts[:boot_order] = %w[hd]
opts[:boot_order].unshift 'network' unless attr[:image_id]

firmware_type = opts.delete(:firmware_type).to_s
opts.merge!(process_firmware_attributes(opts[:firmware], firmware_type))

vm = client.servers.new opts
vm.memory = opts[:memory] if opts[:memory]
vm
Expand Down Expand Up @@ -289,7 +292,9 @@ def vm_instance_defaults
:display => { :type => display_type,
:listen => Setting[:libvirt_default_console_address],
:password => random_password(console_password_length(display_type)),
:port => '-1' }
:port => '-1' },
:firmware => 'automatic',
:firmware_features => { "secure-boot" => "no" }
)
end

Expand Down Expand Up @@ -326,5 +331,20 @@ def validate_volume_capacity(vol)
raise Foreman::Exception.new(N_("Please specify volume size. You may optionally use suffix 'G' to specify volume size in gigabytes."))
end
end

# Generates Secure Boot settings for Libvirt based on the provided firmware type.
# The `secure_boot` setting is used to properly configure and display the Firmware in the `compute_attributes` form.
#
# @param firmware [String] The firmware type.
# @return [Hash] A hash with secure boot settings if applicable.
def generate_secure_boot_settings(firmware)
return {} unless firmware == 'uefi_secure_boot'

{
firmware_features: { 'secure-boot' => 'yes', 'enrolled-keys' => 'yes' },
loader: { 'secure' => 'yes' },
secure_boot: true,
}
end
end
end
45 changes: 29 additions & 16 deletions app/models/compute_resources/foreman/model/vmware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,6 @@ def storage_controller_types
}
end

def firmware_types
{
"automatic" => N_("Automatic"),
"bios" => N_("BIOS"),
"efi" => N_("EFI"),
}
end

def disk_mode_types
{
"persistent" => _("Persistent"),
Expand Down Expand Up @@ -487,8 +479,9 @@ def parse_args(args)

args.except!(:hardware_version) if args[:hardware_version] == 'Default'

firmware_type = args.delete(:firmware_type)
args[:firmware] = firmware_mapping(firmware_type) if args[:firmware] == 'automatic'
firmware_type = args.delete(:firmware_type).to_s
args.merge!(process_firmware_attributes(args[:firmware], firmware_type))
args[:virtual_tpm] = validate_tpm_compatibility(args[:virtual_tpm], args[:firmware])

args.reject! { |k, v| v.nil? }
args
Expand Down Expand Up @@ -521,7 +514,7 @@ def create_vm(args = { })
clone_vm(args)
else
vm = new_vm(args)
vm.firmware = 'bios' if vm.firmware == 'automatic'
raise ArgumentError, errors.full_messages.join(';') if errors.any?
vm.save
end
rescue Fog::Vsphere::Compute::NotFound => e
Expand Down Expand Up @@ -827,11 +820,6 @@ def vm_instance_defaults
)
end

def firmware_mapping(firmware_type)
return 'efi' if firmware_type == :uefi
'bios'
end

def set_vm_volumes_attributes(vm, vm_attrs)
volumes = vm.volumes || []
vm_attrs[:volumes_attributes] = Hash[volumes.each_with_index.map { |volume, idx| [idx.to_s, volume.attributes.merge(:size_gb => volume.size_gb)] }]
Expand Down Expand Up @@ -862,5 +850,30 @@ def valid_cloudinit_for_customspec?(cloudinit)
def cachekey_with_cluster(key, cluster_id = nil)
cluster_id.nil? ? key.to_sym : "#{key}-#{cluster_id}".to_sym
end

# Generates Secure Boot settings for VMware, based on the provided firmware type.
#
# @param firmware [String] The firmware type.
# @return [Hash] A hash with secure boot settings if applicable.
def generate_secure_boot_settings(firmware)
firmware == 'uefi_secure_boot' ? { "secure_boot" => true } : {}
end

# Validates TPM compatibility based on the firmware type and virtual TPM setting.
# Adds an error if TPM is enabled with BIOS firmware, which is incompatible.
# This error is later raised as an `ArgumentError` in the `#create_vm` method.
#
# @param virtual_tpm [String] indicates if the virtual TPM is enabled ('1') or disabled ('0').
# @param firmware [String] the firmware type.
# @return [Boolean] the cast value of virtual_tpm after validation.
def validate_tpm_compatibility(virtual_tpm, firmware)
virtual_tpm = ActiveModel::Type::Boolean.new.cast(virtual_tpm)

if virtual_tpm && firmware == 'bios'
errors.add :base, _('TPM is not compatible with BIOS firmware. Please change Firmware or disable TPM.')
end

virtual_tpm
end
end
end
2 changes: 2 additions & 0 deletions app/models/concerns/pxe_loader_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def firmware_type(pxe_loader)
case pxe_loader
when 'None'
:none
when /SecureBoot/
:uefi_secure_boot
when /UEFI/
:uefi
else
Expand Down
7 changes: 7 additions & 0 deletions app/views/compute_resources_vms/form/libvirt/_base.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
<% checked = params[:host] && params[:host][:compute_attributes] && params[:host][:compute_attributes][:start] || '1' %>
<%= checkbox_f f, :start, { :checked => (checked == '1'), :help_inline => _("Power ON this machine"), :label => _('Start'), :label_size => "col-md-2"} if new_vm && controller_name != "compute_attributes" %>

<% firmware_type = compute_resource.firmware_type(f.object.firmware, f.object.secure_boot) %>
<%= field(f, :firmware, :disabled => !new_vm, :label => _('Firmware'), :label_help => _("Choose 'Automatic' to set the firmware based on the host's PXE Loader. If no PXE Loader is selected, it defaults to BIOS."), :label_size => "col-md-2") do
compute_resource.firmware_types.collect do |type, name|
radio_button_f f, :firmware, {:checked => (firmware_type == type), :disabled => !new_vm, :value => type, :text => _(name) }
end.join(' ').html_safe
end %>

<%
arch ||= nil ; os ||= nil
images = possible_images(compute_resource, arch, os)
Expand Down
12 changes: 10 additions & 2 deletions app/views/compute_resources_vms/form/vmware/_base.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
<%= counter_f(f, :corespersocket, label: _('Cores per socket'), recommended_max_value: compute_resource.max_cpu_count, value: f.object.corespersocket || 1) %>
</div>
<%= text_f f, :memory_mb, :class => "col-md-2", :label => _("Memory (MB)") %>
<%= field(f, :firmware, :label => _('Firmware'), :label_size => "col-md-2") do

<% firmware_type = compute_resource.firmware_type(f.object.firmware, f.object.secure_boot) %>
<%= field(f, :firmware, :label => _('Firmware'), :label_help => _("Choose 'Automatic' to set the firmware based on the PXE Loader. If no PXE Loader is selected, it defaults to BIOS."), :label_size => "col-md-2") do
compute_resource.firmware_types.collect do |type, name|
radio_button_f f, :firmware, {:disabled => !new_vm, :value => type, :text => _(name)}
radio_button_f f, :firmware, { :checked => (firmware_type == type), :disabled => !new_vm, :value => type, :text => _(name) }
end.join(' ').html_safe
end %>
<%= selectable_f f, :cluster, compute_resource.clusters, { :include_blank => _('Please select a cluster') },
Expand Down Expand Up @@ -49,6 +51,12 @@ end %>
{ :disabled => images.empty?, :label => _('Image'), :label_size => "col-md-2" } %>
</div>

<%= checkbox_f f, :virtual_tpm, { :help_inline => _("Add Virtual TPM module to the VM."),
:label => _('Virtual TPM'),
:label_help => _("Only compatible with UEFI firmware."),
:label_size => "col-md-2",
:disabled => !new_vm } %>

<%= compute_specific_js(compute_resource, "nic_info") %>

<%= react_component('StorageContainer', { data: {
Expand Down
4 changes: 3 additions & 1 deletion bundler.d/libvirt.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
group :libvirt do
gem 'fog-libvirt', '>= 0.12.0'
# gem 'fog-libvirt', '>= 0.12.0'
gem 'ruby-libvirt', '~> 0.5', :require => 'libvirt'
# TODO: Remove this line after merging the PR
gem 'fog-libvirt', github: 'nofaralfasi/fog-libvirt', branch: 'sb_libvirt'
end
69 changes: 69 additions & 0 deletions test/models/compute_resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,73 @@ def setup
assert_equal volume_attributes, volumes
end
end

describe '#firmware_type' do
before do
@cr = compute_resources(:mycompute)
end

test "returns firmware unchanged when firmware is not 'efi'" do
assert_equal 'bios', @cr.firmware_type('bios', true)
assert_equal 'bios', @cr.firmware_type('bios', false)
assert_empty(@cr.firmware_type('', true))
assert_nil(@cr.firmware_type(nil, false))
end

test "returns 'uefi' when firmware is 'efi' and secure boot is not enabled" do
assert_equal 'uefi', @cr.firmware_type('efi', false)
assert_equal 'uefi', @cr.firmware_type('efi', nil)
end

test "returns 'uefi_secure_boot' when firmware is 'efi' and secure boot is enabled" do
assert_equal 'uefi_secure_boot', @cr.firmware_type('efi', true)
end
end

describe '#normalize_firmware_type' do
before do
@cr = compute_resources(:mycompute)
end

test "returns 'efi' when firmware is 'uefi'" do
assert_equal 'efi', @cr.normalize_firmware_type('uefi')
end

test "returns 'bios' for non-uefi firmware types" do
assert_equal 'bios', @cr.normalize_firmware_type('bios')
assert_equal 'bios', @cr.normalize_firmware_type('none')
assert_equal 'bios', @cr.normalize_firmware_type('')
assert_equal 'bios', @cr.normalize_firmware_type(nil)
end

test "returns 'efi' when firmware is 'uefi_secure_boot'" do
assert_equal 'efi', @cr.normalize_firmware_type('uefi_secure_boot')
end
end

describe '#resolve_automatic_firmware' do
before do
@cr = compute_resources(:mycompute)
end

test "returns firmware_type when firmware is 'automatic' and firmware_type is present" do
assert_equal 'uefi', @cr.send(:resolve_automatic_firmware, 'automatic', 'uefi')
assert_equal 'bios', @cr.send(:resolve_automatic_firmware, 'automatic', 'bios')
assert_equal 'none', @cr.send(:resolve_automatic_firmware, 'automatic', 'none')
assert_equal 'uefi_secure_boot', @cr.send(:resolve_automatic_firmware, 'automatic', 'uefi_secure_boot')
end

test "returns firmware unchanged when not 'automatic'" do
assert_equal 'uefi', @cr.send(:resolve_automatic_firmware, 'uefi', 'bios')
assert_equal 'bios', @cr.send(:resolve_automatic_firmware, 'bios', 'uefi')
assert_equal 'uefi', @cr.send(:resolve_automatic_firmware, 'uefi', false)
assert_equal 'bios', @cr.send(:resolve_automatic_firmware, 'bios', '')
assert_equal 'uefi_secure_boot', @cr.send(:resolve_automatic_firmware, 'uefi_secure_boot', '')
end

test "returns 'bios' when firmware is 'automatic' and firmware_type is not present" do
assert_equal 'bios', @cr.send(:resolve_automatic_firmware, 'automatic', '')
assert_equal 'bios', @cr.send(:resolve_automatic_firmware, 'automatic', nil)
end
end
end
22 changes: 22 additions & 0 deletions test/models/compute_resources/libvirt_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,26 @@
check_vm_attribute_names(cr)
end
end

describe '#generate_secure_boot_settings' do
before do
@cr = FactoryBot.build_stubbed(:libvirt_cr)
end

test "returns secure boot settings when firmware is 'uefi_secure_boot'" do
expected_sb_settings = {
firmware_features: { 'secure-boot' => 'yes', 'enrolled-keys' => 'yes' },
loader: { 'secure' => 'yes' },
}

assert_equal expected_sb_settings, @cr.send(:generate_secure_boot_settings, 'uefi_secure_boot')

Check failure on line 285 in test/models/compute_resources/libvirt_test.rb

View workflow job for this annotation

GitHub Actions / test:units - Ruby 2.7 and Node 18 on PostgreSQL 13

Failure: test_0001_returns secure boot settings when firmware is 'uefi_secure_boot' --- expected +++ actual @@ -1 +1 @@ -{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}} +{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}, :secure_boot=>true}

Check failure on line 285 in test/models/compute_resources/libvirt_test.rb

View workflow job for this annotation

GitHub Actions / test:units - Ruby 3.0 and Node 18 on PostgreSQL 13

Failure: test_0001_returns secure boot settings when firmware is 'uefi_secure_boot' --- expected +++ actual @@ -1 +1 @@ -{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}} +{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}, :secure_boot=>true}

Check failure on line 285 in test/models/compute_resources/libvirt_test.rb

View workflow job for this annotation

GitHub Actions / test:units - Ruby 3.0 and Node 14 on PostgreSQL 13

Failure: test_0001_returns secure boot settings when firmware is 'uefi_secure_boot' --- expected +++ actual @@ -1 +1 @@ -{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}} +{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}, :secure_boot=>true}

Check failure on line 285 in test/models/compute_resources/libvirt_test.rb

View workflow job for this annotation

GitHub Actions / test:units - Ruby 2.7 and Node 14 on PostgreSQL 13

Failure: test_0001_returns secure boot settings when firmware is 'uefi_secure_boot' --- expected +++ actual @@ -1 +1 @@ -{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}} +{:firmware_features=>{"secure-boot"=>"yes", "enrolled-keys"=>"yes"}, :loader=>{"secure"=>"yes"}, :secure_boot=>true}
end

test "returns an empty hash for firmware types other than 'uefi_secure_boot'" do
assert_empty @cr.send(:generate_secure_boot_settings, 'uefi')
assert_empty @cr.send(:generate_secure_boot_settings, 'bios')
assert_empty @cr.send(:generate_secure_boot_settings, '')
assert_empty @cr.send(:generate_secure_boot_settings, nil)
end
end
end
Loading
Loading