Skip to content

Commit

Permalink
Fix host creation resource origin
Browse files Browse the repository at this point in the history
* Add new resource origin ComputeAttributesOrigin
  -> determine host resources during host creation
* In ResourceOrigins, iterate hosts by name instead of id
  -> new host has no id, but a name
* Add resource quota exception classes
* Add host managed extensions test
* Add host managed factory extensions
* Fix JS deepCopy null error
* Fix autoload filepaths

Verify host creation runs for Libvirt and VMWare.

Co-authored-by: Nadja Heitmann <[email protected]>
  • Loading branch information
bastian-src and nadjaheitmann committed Apr 16, 2024
1 parent c22335f commit 993ee9f
Show file tree
Hide file tree
Showing 19 changed files with 941 additions and 228 deletions.
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ Naming/VariableNumber:
Exclude:
- 'test/**/*.rb'

Naming/FileName:
Exclude:
- 'db/seeds.d/**/*'

Rails/SkipsModelValidations:
Exclude:
- 'db/migrate/**/*'
- 'db/seeds.d/**/*'

Style/FormatStringToken:
Enabled: false
110 changes: 88 additions & 22 deletions app/helpers/foreman_resource_quota/resource_quota_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,106 @@

module ForemanResourceQuota
module ResourceQuotaHelper
FACTOR_B_TO_KB = 1024
FACTOR_B_TO_MB = 1024 * 1024
FACTOR_B_TO_GB = 1024 * FACTOR_B_TO_MB

def natural_resource_name_by_type(type)
type_names = { cpu_cores: 'CPU cores', memory_mb: 'Memory (MB)', disk_gb: 'Disk space (GB)' }
type_names[type]
def natural_resource_name_by_type(resource_type)
type_names = { cpu_cores: 'CPU cores', memory_mb: 'Memory', disk_gb: 'Disk space' }
key = resource_type.is_a?(String) ? resource_type.to_sym : resource_type
raise "No natural name for unknown resource type '#{resource_type}'" unless type_names.key?(key)
type_names[key]
end

def build_missing_resources_per_host_list(hosts, quota_utilization)
# missing_res_per_host := { <host_id>: [<list of to be determined resources>] }
# for example: { 1: [ :disk_gb ], 2: [ :cpu_cores, :disk_gb ] }
return {} if hosts.empty? || quota_utilization.empty?
def units_by_type(resource_type)
type_units = {
cpu_cores: [
{ symbol: 'cores', factor: 1 },
],
memory_mb: [
{ symbol: 'MB', factor: 1 },
{ symbol: 'GB', factor: FACTOR_B_TO_KB },
{ symbol: 'TB', factor: FACTOR_B_TO_MB },
],
disk_gb: [
{ symbol: 'GB', factor: 1 },
{ symbol: 'TB', factor: FACTOR_B_TO_KB },
{ symbol: 'PB', factor: FACTOR_B_TO_MB },
],
}
key = resource_type.to_sym
raise "No units for unknown resource type '#{resource_type}'" unless type_units.key?(key)
type_units[key]
end

missing_res_per_host = hosts.map(&:id).index_with { [] }
missing_res_per_host.each_key do |host_id|
quota_utilization.each do |resource|
missing_res_per_host[host_id] << resource unless resource.nil?
end
def find_largest_unit(resource_value, units)
units.reverse_each do |unit|
return unit.values_at(:symbol, :factor) if resource_value >= unit[:factor]
end
missing_res_per_host
units[0].values_at(:symbol, :factor)
end

def utilization_from_resource_origins(resources, hosts, use_compute_resource: true, use_vm_attributes: true,
use_facts: true)
utilization = resources.each.with_object({}) { |key, hash| hash[key] = 0 }
missing_res_per_host = build_missing_resources_per_host_list(hosts, resources)
def resource_value_to_string(resource_value, resource_type)
(symbol, factor) = find_largest_unit(resource_value, units_by_type(resource_type))
unit_applied_value = (resource_value / factor).round(1)
format_text = if (unit_applied_value % 1).zero?
'%.0f %s'
else
'%.1f %s'
end
format(format_text, unit_applied_value, symbol)
end

if use_compute_resource
ResourceOrigin::ComputeResourceOrigin.new.collect_resources!(utilization, missing_res_per_host)
def utilization_from_resource_origins(resources, hosts, custom_resource_origins: nil)
utilization_sum = resources.each.with_object({}) { |key, hash| hash[key] = 0 }
missing_hosts_resources = create_missing_hosts_resources_hash(hosts, resources)
hosts_hash = hosts.index_by(&:name)
resource_classes = custom_resource_origins || default_resource_origin_classes
resource_classes.each do |origin_class|
origin_class.new.collect_resources!(
utilization_sum,
missing_hosts_resources,
hosts_hash
)
end
ResourceOrigin::VMAttributesOrigin.new.collect_resources!(utilization, missing_res_per_host) if use_vm_attributes
ResourceOrigin::FactsOrigin.new.collect_resources!(utilization, missing_res_per_host) if use_facts

[utilization, missing_res_per_host]
[utilization_sum, missing_hosts_resources]
end

private

# Create a Hash that maps resources to host names.
# { <host name>: [<list of to be determined resources>] }
# for example:
# {
# "host_a": {
# [ :cpu_cores, :disk_gb ]
# },
# "host_b": {
# [ :cpu_cores, :disk_gb ]
# },
# Parameters:
# - hosts: Array of host objects.
# - resources: Array of resources (as symbol, e.g. [:cpu_cores, :disk_gb]).
# Returns: Hash with host names as keys and resources as values.
def create_missing_hosts_resources_hash(hosts, resources)
return {} if hosts.empty? || resources.empty?

resources_to_determine = resources.compact
return {} if resources_to_determine.empty?

hosts.map(&:name).index_with { resources_to_determine.clone }
end

# Default classes that are used to determine host resources. Determines
# resources in the order of this list.
def default_resource_origin_classes
[
ResourceOrigins::ComputeResourceOrigin,
ResourceOrigins::VMAttributesOrigin,
ResourceOrigins::ComputeAttributesOrigin,
ResourceOrigins::FactsOrigin,
]
end
end
end
106 changes: 80 additions & 26 deletions app/models/concerns/foreman_resource_quota/host_managed_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
module ForemanResourceQuota
module HostManagedExtensions
extend ActiveSupport::Concern
include ResourceQuotaHelper
include ForemanResourceQuota::ResourceQuotaHelper
include ForemanResourceQuota::Exceptions

included do
validate :check_resource_quota_capacity
Expand All @@ -12,50 +13,103 @@ module HostManagedExtensions
scoped_search relation: :resource_quota, on: :name, complete_value: true, rename: :resource_quota
end

# rubocop: disable Metrics/AbcSize
def check_resource_quota_capacity
return errors.empty? if early_return?
handle_quota_check
true
rescue ResourceQuotaException => e
handle_error('resource_quota_id',
e.bare_message,
format('An error occured while checking the resource quota capacity: %s', e))
rescue Foreman::Exception => e
handle_error(:base,
e.bare_message,
format('An unexpected Foreman error occured while checking the resource quota capacity: %s', e))
rescue StandardError => e
handle_error(:base,
e.message,
format('An unknown error occured while checking the resource quota capacity: %s', e))
end

private

def handle_quota_check
return if early_return?
quota_utilization = determine_quota_utilization
host_resources = determine_host_resources
verify_resource_quota_limits(quota_utilization, host_resources)
end

resource_quota.determine_utilization([self])
unless resource_quota.missing_hosts.empty?
errors.add(:resource_quota,
N_(format('An error occured reading host resources. Check the foreman error log.')))
return false
def handle_error(error_module, error_message, log_message)
errors.add(error_module, error_message)
Rails.logger.error(N_(log_message))
false
end

def determine_quota_utilization
resource_quota.determine_utilization
missing_hosts = resource_quota.missing_hosts
unless missing_hosts.empty?
raise ResourceQuotaUtilizationException,
"Resource Quota '#{resource_quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
end
resource_quota.utilization
end

verify_resource_quota_limits(resource_quota.utilization)
errors.empty?
def determine_host_resources
(host_resources, missing_hosts) = call_utilization_helper(resource_quota.active_resources, [self])
unless missing_hosts.empty?
raise HostResourcesException,
"Cannot determine host resources for #{name}"
end
host_resources
end
# rubocop: enable Metrics/AbcSize

private
def verify_resource_quota_limits(quota_utilization, host_resources)
quota_utilization.each do |resource_type, resource_utilization|
next if resource_utilization.nil?

max_quota = resource_quota[resource_type]
all_hosts_utilization = resource_utilization + host_resources[resource_type.to_sym]
next if all_hosts_utilization <= max_quota

raise ResourceLimitException, formulate_limit_error(resource_utilization,
all_hosts_utilization, max_quota, resource_type)
end
end

def verify_resource_quota_limits(quota_utilization)
quota_utilization.each do |type, utilization|
next if utilization.nil?
max_quota = resource_quota[type]
next if utilization <= max_quota
errors.add(:resource_quota, N_(format("Host resources exceed quota '%s' for %s: %s > %s",
resource_quota_name,
natural_resource_name_by_type(type),
utilization,
max_quota)))
break
def formulate_limit_error(resource_utilization, all_hosts_utilization, max_quota, resource_type)
if resource_utilization < max_quota
N_(format("Host exceeds %s limit of '%s'-quota by %s (max. %s)",
natural_resource_name_by_type(resource_type),
resource_quota.name,
resource_value_to_string(all_hosts_utilization - max_quota, resource_type),
resource_value_to_string(max_quota, resource_type)))
else
N_(format("%s limit of '%s'-quota is already exceeded by %s without adding the new host (max. %s)",
natural_resource_name_by_type(resource_type),
resource_quota.name,
resource_value_to_string(resource_utilization - max_quota, resource_type),
resource_value_to_string(max_quota, resource_type)))
end
end

def early_return?
if resource_quota.nil?
return true if quota_assigment_optional?
errors.add(:resource_quota, 'must be given.')
return true
raise HostResourceQuotaEmptyException, 'must be given.'
end
return true if resource_quota.active_resources.empty?
return true if Setting[:resource_quota_global_no_action] # quota is assigned, but not supposed to be checked
false
end

def quota_assigment_optional?
owner.resource_quota_is_optional || owner.admin || Setting[:resource_quota_global_optional_user_assignment]
owner.resource_quota_is_optional || Setting[:resource_quota_optional_assignment]
end

# Wrap into a function for easier testing
def call_utilization_helper(resources, hosts)
utilization_from_resource_origins(resources, hosts)
end
end
end
20 changes: 10 additions & 10 deletions app/models/foreman_resource_quota/resource_quota.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def number_of_usergroups
usergroups.size
end

def determine_utilization(additional_hosts: [])
def determine_utilization(additional_hosts = [])
quota_hosts = (hosts | (additional_hosts))
self.utilization, self.missing_hosts = call_utilization_helper(quota_hosts)

Expand All @@ -52,13 +52,6 @@ def to_label
name
end

private

# Wrap into a function for better testing
def call_utilization_helper(quota_hosts)
utilization_from_resource_origins(active_resources, quota_hosts)
end

def active_resources
resources = []
%i[cpu_cores memory_mb disk_gb].each do |res|
Expand All @@ -67,11 +60,18 @@ def active_resources
resources
end

private

# Wrap into a function for easier testing
def call_utilization_helper(quota_hosts)
utilization_from_resource_origins(active_resources, quota_hosts)
end

def print_warning(missing_hosts, hosts)
warn_text = "Could not determines resources for #{utilization_hash.missing_hosts.size} hosts:"
warn_text = "Could not determines resources for #{missing_hosts.size} hosts:"
missing_hosts.each do |host_id, missing_resources|
missing_host = hosts.find { |obj| obj.id == host_id }
warn_text << " '#{missing_host.name}': '#{missing_resources}'\n"
warn_text << " '#{missing_host.name}': '#{missing_resources}'\n" unless missing_host.nil?
end
Rails.logger.warn warn_text
end
Expand Down
Loading

0 comments on commit 993ee9f

Please sign in to comment.