diff --git a/.rubocop.yml b/.rubocop.yml index 342509a..f19eac4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,6 +18,10 @@ Metrics/ClassLength: Exclude: - 'app/models/foreman_resource_quota/resource_quota.rb' +Metrics/ModuleLength: + Exclude: + - 'app/models/concerns/foreman_resource_quota/host_managed_extensions.rb' + Metrics/MethodLength: Enabled: false diff --git a/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb b/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb index 5f11432..890a605 100644 --- a/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +++ b/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb @@ -64,7 +64,6 @@ def usergroups def_param_group :resource_quota do param :resource_quota, Hash, required: true, action_aware: true do param :name, String, required: true - # param :operatingsystem_ids, Array, :desc => N_("Operating system IDs") end end diff --git a/app/helpers/foreman_resource_quota/resource_quota_helper.rb b/app/helpers/foreman_resource_quota/resource_quota_helper.rb index 683ef40..119d004 100644 --- a/app/helpers/foreman_resource_quota/resource_quota_helper.rb +++ b/app/helpers/foreman_resource_quota/resource_quota_helper.rb @@ -52,20 +52,29 @@ def resource_value_to_string(resource_value, resource_type) format(format_text, unit_applied_value, symbol) end + # Use different resource origins to determine host resource utilization. + # - iterates all given hosts and tries do determine their resources utilization + # Returns: + # [ , ] + # for example: + # [ + # { "host_a": { cpu_cores: 20, memory_mb: 8196 }, "host_b": { cpu_cores: 15, memory_mb: nil } }, + # { "host_c": [ :memory_mb ] }, + # ] def utilization_from_resource_origins(resources, hosts, custom_resource_origins: nil) - utilization_sum = resources.each.with_object({}) { |key, hash| hash[key] = 0 } + hosts_resources = create_hosts_resources_hash(hosts, resources) 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, + hosts_resources, missing_hosts_resources, hosts_hash ) end - [utilization_sum, missing_hosts_resources] + [hosts_resources, missing_hosts_resources] end private @@ -89,6 +98,25 @@ def create_missing_hosts_resources_hash(hosts, resources) hosts.map(&:name).index_with { resources_to_determine.clone } end + # Create a Hash that maps resources and a value to host names. + # { : {} } + # for example: + # { + # "host_a": { cpu_cores: nil, disk_gb: nil }, + # "host_b": { cpu_cores: nil, disk_gb: nil }, + # } + # 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 resource-hashs as values. + def create_hosts_resources_hash(hosts, resources) + return {} if hosts.empty? || resources.empty? + + # Create a hash template with resources mapped to nil + resources_to_determine = resources.index_with { |_resource| nil } + hosts.map(&:name).index_with { resources_to_determine.dup } + end + # Default classes that are used to determine host resources. Determines # resources in the order of this list. def default_resource_origin_classes diff --git a/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb b/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb index 763232b..989a555 100644 --- a/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +++ b/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb @@ -7,16 +7,22 @@ module HostManagedExtensions include ForemanResourceQuota::Exceptions included do - validate :check_resource_quota_capacity - - belongs_to :resource_quota, class_name: '::ForemanResourceQuota::ResourceQuota' - has_one :resource_quota_missing_resources, class_name: '::ForemanResourceQuota::ResourceQuotaMissingHost', - inverse_of: :missing_host, foreign_key: :missing_host_id, dependent: :destroy + validate :verify_resource_quota + + has_one :host_resources, class_name: '::ForemanResourceQuota::HostResources', + inverse_of: :host, foreign_key: :host_id, dependent: :destroy + has_one :resource_quota_host, class_name: '::ForemanResourceQuota::ResourceQuotaHost', + inverse_of: :host, foreign_key: :host_id, dependent: :destroy + has_one :resource_quota, class_name: '::ForemanResourceQuota::ResourceQuota', + through: :resource_quota_host scoped_search relation: :resource_quota, on: :name, complete_value: true, rename: :resource_quota + + # A host shall always have a .host_resources attribute + before_validation :build_host_resources, unless: -> { host_resources.present? } end - def check_resource_quota_capacity - handle_quota_check + def verify_resource_quota + handle_quota_check(resource_quota) true rescue ResourceQuotaException => e handle_error('resource_quota_id', @@ -32,13 +38,27 @@ def check_resource_quota_capacity format('An unknown error occured while checking the resource quota capacity: %s', e)) end + def resource_quota_id + resource_quota&.id + end + + def resource_quota_id=(val) + if val.blank? + resource_quota_host&.destroy + else + quota = ForemanResourceQuota::ResourceQuota.find_by(id: val) + raise ActiveRecord::RecordNotFound, "ResourceQuota with ID \"#{val}\" not found" unless quota + self.resource_quota = quota + end + 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) + def handle_quota_check(quota) + return if early_return?(quota) + quota_utilization = determine_quota_utilization(quota) + current_host_resources = determine_host_resources(quota.active_resources) + check_resource_quota_limits(quota, quota_utilization, current_host_resources) end def handle_error(error_module, error_message, log_message) @@ -47,60 +67,72 @@ def handle_error(error_module, error_message, log_message) false end - def determine_quota_utilization - resource_quota.determine_utilization - missing_hosts = resource_quota.missing_hosts + def determine_quota_utilization(quota) + missing_hosts = quota.missing_hosts(exclude: [name]) unless missing_hosts.empty? raise ResourceQuotaUtilizationException, - "Resource Quota '#{resource_quota.name}' cannot determine resources for #{missing_hosts.size} hosts." + "Resource Quota '#{quota.name}' cannot determine resources for #{missing_hosts.size} hosts." end - resource_quota.utilization + quota.utilization(exclude: [name]) end - def determine_host_resources - (host_resources, missing_hosts) = call_utilization_helper(resource_quota.active_resources, [self]) - unless missing_hosts.empty? + def determine_host_resources(active_resources) + new_host_resources, missing_hosts = call_utilization_helper(active_resources, [self]) + if missing_hosts.key?(name) || missing_hosts.key?(name.to_sym) raise HostResourcesException, - "Cannot determine host resources for #{name}" + "Cannot determine host resources for #{name}: #{missing_hosts[name]}" end - host_resources + host_resources.resources = new_host_resources + host_resources.resources end - def verify_resource_quota_limits(quota_utilization, host_resources) + def check_resource_quota_limits(quota, quota_utilization, current_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] + max_quota = quota[resource_type] + all_hosts_utilization = resource_utilization + current_host_resources[resource_type.to_sym] next if all_hosts_utilization <= max_quota - raise ResourceLimitException, formulate_limit_error(resource_utilization, + raise ResourceLimitException, formulate_limit_error(quota.name, resource_utilization, all_hosts_utilization, max_quota, resource_type) end end - def formulate_limit_error(resource_utilization, all_hosts_utilization, max_quota, resource_type) - if resource_utilization < max_quota + def formulate_limit_error(quota_name, 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, + 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, + 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? + def formulate_resource_inconsistency_error(quota_name, resource_type, quota_utilization_value, resource_value) + N_("Resource Quota '#{quota_name}' inconsistency detected while destroying host '#{name}':\n" \ + "Resource Quota #{resource_type} current utilization: #{quota_utilization_value}.\n" \ + "Host resource value: #{resource_value}.\n" \ + 'Skipping.') + end + + def formulate_quota_inconsistency_error(quota_name) + N_("An error occured adapting the resource quota utilization of '#{quota_name}' " \ + "while processing host '#{name}'. The resource quota utilization values might be inconsistent.") + end + + def early_return?(quota) + if quota.nil? return true if quota_assigment_optional? raise HostResourceQuotaEmptyException, 'must be given.' end - return true if resource_quota.active_resources.empty? + return true if quota.active_resources.empty? return true if Setting[:resource_quota_global_no_action] # quota is assigned, but not supposed to be checked false end @@ -111,7 +143,13 @@ def quota_assigment_optional? # Wrap into a function for easier testing def call_utilization_helper(resources, hosts) - utilization_from_resource_origins(resources, hosts) + all_host_resources, missing_hosts = utilization_from_resource_origins(resources, hosts) + unless all_host_resources.key?(name) + raise HostResourcesException, + "Host #{name} was not included when determining host resources." + end + current_host_resources = all_host_resources[name] + [current_host_resources, missing_hosts] end end end diff --git a/app/models/foreman_resource_quota/host_resources.rb b/app/models/foreman_resource_quota/host_resources.rb new file mode 100644 index 0000000..7230c50 --- /dev/null +++ b/app/models/foreman_resource_quota/host_resources.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module ForemanResourceQuota + class HostResources < ApplicationRecord + self.table_name = 'hosts_resources' + + belongs_to :host, class_name: '::Host::Managed' + validates :host, { presence: true, uniqueness: true } + + def resources + { + cpu_cores: cpu_cores, + memory_mb: memory_mb, + disk_gb: disk_gb, + } + end + + def resources=(val) + allowed_attributes = val.slice(:cpu_cores, :memory_mb, :disk_gb) + assign_attributes(allowed_attributes) # Set multiple attributes at once (given a hash) + end + + # Returns an array of unknown host resources (returns an empty array if all are known) + # For example, completely unknown host resources returns: + # [ + # :cpu_cores, + # :memory_mb, + # :disk_gb, + # ] + # Consider only the resource_quota's active resources by default. + def missing_resources(only_active_resources: true) + empty_resources = [] + resources_to_check = %i[cpu_cores memory_mb disk_gb] + resources_to_check = host.resource_quota.active_resources if only_active_resources && host.resource_quota.present? + + resources_to_check.each do |single_resource| + empty_resources << single_resource if send(single_resource).nil? + end + + empty_resources + end + end +end diff --git a/app/models/foreman_resource_quota/resource_quota.rb b/app/models/foreman_resource_quota/resource_quota.rb index 79b0c24..509f5ae 100644 --- a/app/models/foreman_resource_quota/resource_quota.rb +++ b/app/models/foreman_resource_quota/resource_quota.rb @@ -12,12 +12,12 @@ class ResourceQuota < ApplicationRecord self.table_name = 'resource_quotas' + has_many :resource_quotas_hosts, class_name: 'ResourceQuotaHost', inverse_of: :resource_quota, dependent: :destroy has_many :resource_quotas_users, class_name: 'ResourceQuotaUser', inverse_of: :resource_quota, dependent: :destroy has_many :resource_quotas_usergroups, class_name: 'ResourceQuotaUsergroup', inverse_of: :resource_quota, dependent: :destroy - has_many :resource_quotas_missing_hosts, class_name: 'ResourceQuotaMissingHost', inverse_of: :resource_quota, - dependent: :destroy - has_many :hosts, class_name: '::Host::Managed', dependent: :nullify + has_many :hosts, -> { distinct }, class_name: '::Host::Managed', through: :resource_quotas_hosts + has_many :hosts_resources, class_name: 'HostResources', through: :hosts has_many :users, class_name: '::User', through: :resource_quotas_users has_many :usergroups, class_name: '::Usergroup', through: :resource_quotas_usergroups @@ -27,7 +27,7 @@ class ResourceQuota < ApplicationRecord scoped_search on: :id, complete_enabled: false, only_explicit: true, validator: ScopedSearch::Validators::INTEGER def number_of_hosts - hosts.size + hosts_resources.size end def number_of_users @@ -49,54 +49,84 @@ def number_of_missing_hosts # "host_a": [ :cpu_cores, :disk_gb ], # "host_b": [ :memory_mb ], # } - def missing_hosts - # Initialize default value as an empty array - missing_hosts_list = Hash.new { |hash, key| hash[key] = [] } - resource_quotas_missing_hosts.each do |missing_host_rel| - host_name = missing_host_rel.missing_host.name - missing_hosts_list[host_name] << :cpu_cores if missing_host_rel.no_cpu_cores - missing_hosts_list[host_name] << :memory_mb if missing_host_rel.no_memory_mb - missing_hosts_list[host_name] << :disk_gb if missing_host_rel.no_disk_gb + # Parameters: + # - exclude: an Array of host names to exclude from the utilization + def missing_hosts(exclude: []) + missing_hosts = {} + active_resources.each do |single_resource| + hosts_resources.where(single_resource => nil).includes([:host]).find_each do |host_resources_item| + host_name = host_resources_item.host.name + next if exclude.include?(host_name) + missing_hosts[host_name] ||= [] + missing_hosts[host_name] << single_resource + end end - missing_hosts_list + missing_hosts end - # Set the hosts that are listed in resource_quotas_missing_hosts + # Returns a Hash with the quota resources and their utilization as key-value pair + # It returns always all resources, even if they are not used (nil in that case). + # For example: + # { + # cpu_cores: 10, + # memory_mb: nil, + # disk_gb: 20, + # } # Parameters: - # - val: Hash of host names and list of missing resources - # { : [] } - # for example: - # { - # "host_a": [ :cpu_cores, :disk_gb ], - # "host_b": [ :memory_mb ], - # } - def missing_hosts=(val) - # Delete all entries and write new ones - resource_quotas_missing_hosts.delete_all - val.each do |host_name, missing_resources| - add_missing_host(host_name, missing_resources) + # - exclude: an Array of host names to exclude from the utilization + def utilization(exclude: []) + current_utilization = { + cpu_cores: nil, + memory_mb: nil, + disk_gb: nil, + } + + active_resources.each do |resource| + current_utilization[resource] = 0 + end + + hosts_resources.each do |host_resources_item| + next if exclude.include?(host_resources_item.host.name) + + active_resources.each do |resource| + current_utilization[resource] += host_resources_item.send(resource).to_i + end end + + current_utilization end - def utilization - { - cpu_cores: utilization_cpu_cores, - memory_mb: utilization_memory_mb, - disk_gb: utilization_disk_gb, - } + def hosts_resources_as_hash + resources_hash = hosts.map(&:name).index_with { {} } + hosts_resources.each do |host_resources_item| + active_resources do |resource_name| + resources_hash[host_resources_item.host.name][resource_name] = host_resources_item.send(resource_name) + end + end + resources_hash end - def utilization=(val) - update_single_utilization(:cpu_cores, val) - update_single_utilization(:memory_mb, val) - update_single_utilization(:disk_gb, val) + def update_hosts_resources(hosts_resources_hash) + # Only update hosts that are associated with this quota + update_hosts = hosts.where(name: hosts_resources_hash.keys) + update_hosts_ids = update_hosts.pluck(:name, :id).to_h + hosts_resources_hash.each do |host_name, resources| + # Update the host_resources without loading the whole host object + host_resources_item = hosts_resources.find_by(host_id: update_hosts_ids[host_name]) + if host_resources_item + host_resources_item.resources = resources + host_resources_item.save + else + Rails.logger.warn "HostResources not found for host_name: #{host_name}" + end + end end def determine_utilization(additional_hosts = []) quota_hosts = (hosts | (additional_hosts)) - quota_utilization, missing_hosts_resources = call_utilization_helper(quota_hosts) - update(utilization: quota_utilization) - update(missing_hosts: missing_hosts_resources) + all_host_resources, missing_hosts_resources = call_utilization_helper(quota_hosts) + update_hosts_resources(all_host_resources) + Rails.logger.warn create_hosts_resources_warning(missing_hosts_resources) unless missing_hosts.empty? rescue StandardError => e Rails.logger.error("An error occured while determining resources for quota '#{name}': #{e}") @@ -128,25 +158,5 @@ def create_hosts_resources_warning(missing_hosts_resources) warn_text << " '#{host_name}': '#{missing_resources}'\n" unless missing_resources.empty? end end - - def update_single_utilization(attribute, val) - return unless val.key?(attribute.to_sym) || val.key?(attribute.to_s) - update("utilization_#{attribute}": val[attribute.to_sym] || val[attribute.to_s]) - end - - def add_missing_host(host_name, missing_resources) - return if missing_resources.empty? - - host = Host::Managed.find_by(name: host_name) - raise HostNotFoundException if host.nil? - - resource_quotas_missing_hosts << ResourceQuotaMissingHost.new( - missing_host: host, - resource_quota: self, - no_cpu_cores: missing_resources.include?(:cpu_cores), - no_memory_mb: missing_resources.include?(:memory_mb), - no_disk_gb: missing_resources.include?(:disk_gb) - ) - end end end diff --git a/app/models/foreman_resource_quota/resource_quota_host.rb b/app/models/foreman_resource_quota/resource_quota_host.rb new file mode 100644 index 0000000..59725c7 --- /dev/null +++ b/app/models/foreman_resource_quota/resource_quota_host.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ForemanResourceQuota + class ResourceQuotaHost < ApplicationRecord + self.table_name = 'resource_quotas_hosts' + + belongs_to :resource_quota, class_name: 'ResourceQuota' + belongs_to :host, class_name: '::Host::Managed' + end +end diff --git a/app/models/foreman_resource_quota/resource_quota_missing_host.rb b/app/models/foreman_resource_quota/resource_quota_missing_host.rb deleted file mode 100644 index 00786b8..0000000 --- a/app/models/foreman_resource_quota/resource_quota_missing_host.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module ForemanResourceQuota - class ResourceQuotaMissingHost < ApplicationRecord - self.table_name = 'resource_quotas_missing_hosts' - - belongs_to :resource_quota, inverse_of: :resource_quotas_missing_hosts - belongs_to :missing_host, class_name: '::Host::Managed', inverse_of: :resource_quota_missing_resources - end -end diff --git a/app/services/foreman_resource_quota/resource_origin.rb b/app/services/foreman_resource_quota/resource_origin.rb index 81f662e..d6c554e 100644 --- a/app/services/foreman_resource_quota/resource_origin.rb +++ b/app/services/foreman_resource_quota/resource_origin.rb @@ -8,12 +8,12 @@ class ResourceOrigin disk_gb: :extract_disk_gb, }.freeze - def collect_resources!(resources_sum, missing_hosts_resources, host_objects) + def collect_resources!(hosts_resources, missing_hosts_resources, host_objects) return if missing_hosts_resources.empty? relevant_hosts = load_hosts_eagerly(missing_hosts_resources, host_objects, host_attribute_eager_name) host_values = collect_attribute_from_hosts(relevant_hosts, host_attribute_name) - sum_resources_and_delete_missing_hosts!(resources_sum, missing_hosts_resources, host_values) + process_resources_and_delete_missing_hosts!(hosts_resources, missing_hosts_resources, host_values) end def host_attribute_eager_name @@ -65,12 +65,12 @@ def collect_attribute_from_hosts(host_list, attribute_name) host_values end - def sum_resources_and_delete_missing_hosts!(resources_sum, missing_hosts_resources, host_values) + def process_resources_and_delete_missing_hosts!(hosts_resources, missing_hosts_resources, host_values) host_values.each do |host_name, attribute_content| missing_hosts_resources[host_name].reverse_each do |resource_name| resource_value = process_resource(resource_name, attribute_content) next unless resource_value - resources_sum[resource_name] += resource_value + hosts_resources[host_name][resource_name] = resource_value missing_hosts_resources[host_name].delete(resource_name) end missing_hosts_resources.delete(host_name) if missing_hosts_resources[host_name].empty? diff --git a/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb b/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb index 57ed4bb..5e62770 100644 --- a/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +++ b/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb @@ -20,7 +20,7 @@ def extract_disk_gb(_) nil end - def collect_resources!(resources_sum, missing_hosts_resources, _host_objects) + def collect_resources!(hosts_resources, missing_hosts_resources, _host_objects) compute_resource_to_hosts = group_hosts_by_compute_resource(missing_hosts_resources.keys) compute_resource_to_hosts.each do |compute_resource_id, hosts| @@ -32,7 +32,7 @@ def collect_resources!(resources_sum, missing_hosts_resources, _host_objects) hosts.each do |host| vm = host_vms.find { |obj| obj.send(vm_id_attr) == host.uuid } next unless vm - process_host_vm!(resources_sum, missing_hosts_resources, host.name, vm) + process_host_vm!(hosts_resources, missing_hosts_resources, host.name, vm) end end end @@ -63,16 +63,16 @@ def filter_vms_by_hosts(hosts, compute_resource_id) # Processes a host's virtual machines and updates resource allocation. # Parameters: - # - resources_sum: Hash containing total resources sum. + # - hosts_resources: Hash containing successfully determined resources per host. # - missing_hosts_resources: Hash containing missing resources per host. - # - host_name: Name of the host. + # - host: Host object # - vm: Compute resource VM object of given host. # Returns: None. - def process_host_vm!(resources_sum, missing_hosts_resources, host_name, host_vm) + def process_host_vm!(hosts_resources, missing_hosts_resources, host_name, host_vm) missing_hosts_resources[host_name].reverse_each do |resource_name| resource_value = process_resource(resource_name, host_vm) next unless resource_value - resources_sum[resource_name] += resource_value + hosts_resources[host_name][resource_name] = resource_value missing_hosts_resources[host_name].delete(resource_name) end missing_hosts_resources.delete(host_name) if missing_hosts_resources[host_name].empty? diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ec56fce..ea918e2 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -2,4 +2,6 @@ ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'resource_quota', 'resource_quotas' + inflect.irregular 'host_resources', 'hosts_resources' + inflect.irregular 'HostResources', 'HostsResources' end diff --git a/db/migrate/20240611141744_remove_utilization_from_resource_quotas.rb b/db/migrate/20240611141744_remove_utilization_from_resource_quotas.rb new file mode 100644 index 0000000..81adbaf --- /dev/null +++ b/db/migrate/20240611141744_remove_utilization_from_resource_quotas.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class RemoveUtilizationFromResourceQuotas < ActiveRecord::Migration[6.1] + def change + remove_column :resource_quotas, :utilization_cpu_cores, :integer + remove_column :resource_quotas, :utilization_memory_mb, :integer + remove_column :resource_quotas, :utilization_disk_gb, :integer + end +end diff --git a/db/migrate/20240611141939_drop_missing_hosts.rb b/db/migrate/20240611141939_drop_missing_hosts.rb new file mode 100644 index 0000000..83222bb --- /dev/null +++ b/db/migrate/20240611141939_drop_missing_hosts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DropMissingHosts < ActiveRecord::Migration[6.1] + def up + drop_table :resource_quotas_missing_hosts + end +end diff --git a/db/migrate/20240611142813_create_hosts_resources.rb b/db/migrate/20240611142813_create_hosts_resources.rb new file mode 100644 index 0000000..3f4e5fe --- /dev/null +++ b/db/migrate/20240611142813_create_hosts_resources.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreateHostsResources < ActiveRecord::Migration[6.1] + def change + create_table :hosts_resources do |t| + t.belongs_to :host, index: { unique: true }, foreign_key: true, null: false + t.integer :cpu_cores, default: nil + t.integer :memory_mb, default: nil + t.integer :disk_gb, default: nil + + t.timestamps + end + + create_table :resource_quotas_hosts do |t| + t.belongs_to :host, index: { unique: true }, foreign_key: true, null: false + t.belongs_to :resource_quota, foreign_key: true, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20240618163434_remove_resource_quota_from_hosts.rb b/db/migrate/20240618163434_remove_resource_quota_from_hosts.rb new file mode 100644 index 0000000..30a2997 --- /dev/null +++ b/db/migrate/20240618163434_remove_resource_quota_from_hosts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveResourceQuotaFromHosts < ActiveRecord::Migration[6.1] + def change + remove_reference :hosts, :resource_quota, foreign_key: true + end +end diff --git a/test/controllers/api/v2/resource_quotas_test.rb b/test/controllers/api/v2/resource_quotas_test.rb index a690bdb..03270a8 100644 --- a/test/controllers/api/v2/resource_quotas_test.rb +++ b/test/controllers/api/v2/resource_quotas_test.rb @@ -123,7 +123,7 @@ def setup test 'should show utilization' do exp_utilization = { cpu_cores: 10, memory_mb: 20 } - stub_quota_utilization(exp_utilization, {}) + stub_quota_utilization(exp_utilization) get :utilization, params: { resource_quota_id: @quota.id }, session: set_session_user assert_response :success show_response = ActiveSupport::JSON.decode(@response.body) @@ -134,7 +134,7 @@ def setup test 'should show missing_hosts' do exp_missing_hosts = { 'some_host' => %i[cpu_cores memory_mb] } - stub_quota_utilization({}, exp_missing_hosts) + stub_quota_missing_hosts(exp_missing_hosts) get :missing_hosts, params: { resource_quota_id: @quota.id }, session: set_session_user assert_response :success show_response = ActiveSupport::JSON.decode(@response.body) diff --git a/test/factories/foreman_resource_quota_factories.rb b/test/factories/foreman_resource_quota_factories.rb index 2f45852..b2ce35e 100644 --- a/test/factories/foreman_resource_quota_factories.rb +++ b/test/factories/foreman_resource_quota_factories.rb @@ -5,5 +5,23 @@ sequence(:name) { |n| "test resource quota#{n}" } sequence(:description) { |n| "resource quota description#{n}" } end - # TODO: Evaluate adding fixtures for resource origins + + trait :with_existing_host_resources do + transient do + host_resources { [] } + end + + after(:create) do |quota, evaluator| + quota.cpu_cores = nil + quota.memory_mb = nil + quota.disk_gb = nil + host = FactoryBot.create(:host, resource_quota: quota) + host.host_resources.resources = evaluator.host_resources + host.host_resources.save! + quota.cpu_cores = evaluator.cpu_cores + quota.memory_mb = evaluator.memory_mb + quota.disk_gb = evaluator.disk_gb + quota.save! + end + end end diff --git a/test/models/concerns/host_managed_extension_test.rb b/test/models/concerns/host_managed_extension_test.rb index 4f42e58..b116237 100644 --- a/test/models/concerns/host_managed_extension_test.rb +++ b/test/models/concerns/host_managed_extension_test.rb @@ -6,16 +6,66 @@ module ForemanResourceQuota class HostManagedExtensionTest < ActiveSupport::TestCase include ForemanResourceQuota::ResourceQuotaHelper - describe 'host create validation' do - def setup - # Set relevant settings explicitly - Setting[:resource_quota_global_no_action] = false - Setting[:resource_quota_optional_assignment] = false - User.current.resource_quota_is_optional = false + def setup + # Set relevant settings explicitly + Setting[:resource_quota_global_no_action] = false + Setting[:resource_quota_optional_assignment] = false + User.current.resource_quota_is_optional = false + end + + describe 'assign resource quota' do + test 'assign a resource quota' do + Setting[:resource_quota_optional_assignment] = true + host = FactoryBot.create(:host) + quota = FactoryBot.create(:resource_quota) + host.resource_quota = quota + assert_equal host.resource_quota_host.resource_quota_id, quota.id + assert_equal host.resource_quota_id, quota.id + end + + test 'disassociate resource quota via .resource_quota' do + Setting[:resource_quota_optional_assignment] = true + host = FactoryBot.create(:host) + quota = FactoryBot.create(:resource_quota) + host.resource_quota = quota + assert_equal host.resource_quota_host.resource_quota_id, quota.id + assert_equal host.resource_quota_id, quota.id + host.resource_quota = nil # destroy the association + host.reload + assert_nil host.resource_quota_host + assert_nil host.resource_quota + end + + test 'disassociate resource quota via .resource_quota_id to nil' do + Setting[:resource_quota_optional_assignment] = true + host = FactoryBot.create(:host) + quota = FactoryBot.create(:resource_quota) + host.resource_quota = quota + assert_equal host.resource_quota_host.resource_quota_id, quota.id + assert_equal host.resource_quota_id, quota.id + host.resource_quota_id = nil # destroy the association + host.reload + assert_nil host.resource_quota_host + assert_nil host.resource_quota + end + + test 'disassociate resource quota via .resource_quota_id to empty string' do + Setting[:resource_quota_optional_assignment] = true + host = FactoryBot.create(:host) + quota = FactoryBot.create(:resource_quota) + host.resource_quota = quota + assert_equal host.resource_quota_host.resource_quota_id, quota.id + assert_equal host.resource_quota_id, quota.id + host.resource_quota_id = '' # destroy the association + host.reload + assert_nil host.resource_quota_host + assert_nil host.resource_quota end + end + describe 'host create validation' do test 'should validate resource quota capacity' do - Host.any_instance.expects(:check_resource_quota_capacity).once + Host.any_instance.expects(:verify_resource_quota).once FactoryBot.create(:host, :with_resource_quota) end @@ -33,172 +83,277 @@ def setup assert FactoryBot.create(:host) end + test 'should have a default host_resources attribute' do + Setting[:resource_quota_optional_assignment] = true + host = FactoryBot.create(:host) + Setting[:resource_quota_optional_assignment] = false + assert_not_nil host.host_resources + end + test 'should fail without resource quota' do assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host) } end end + describe 'host destroy validation' do + test 'should destroy host' do + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { + cpu_cores: 5, + memory_mb: 5 * 1024, + disk_gb: 10, + }, + cpu_cores: 20, + memory_mb: 20 * 1024, + disk_gb: 50) + stub_host_utilization({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, {}) # pass host utilization + + host = FactoryBot.create(:host, resource_quota: quota) + quota.reload + + assert host.save + assert_equal({ cpu_cores: 7, memory_mb: 9 * 1024, disk_gb: 30 }, quota.utilization) + + assert host.destroy + quota.reload + assert_equal({ cpu_cores: 5, memory_mb: 5 * 1024, disk_gb: 10 }, quota.utilization) + end + + test 'missing_hosts are updated on host destroy' do + Setting[:resource_quota_optional_assignment] = true + host_a = FactoryBot.create :host + host_b = FactoryBot.create :host + host_utilization = { + host_a.name => { cpu_cores: 1 }, + host_b.name => { cpu_cores: 1 }, + } + host_missing_resources = { + host_a.name => [:memory_mb], + host_b.name => [:memory_mb], + } + quota = FactoryBot.create(:resource_quota, cpu_cores: 10, memory_mb: 10) + quota.hosts << [host_a, host_b] + quota.save + + quota.stub(:call_utilization_helper, [host_utilization, host_missing_resources]) do + quota.determine_utilization + end + assert_equal quota.number_of_hosts, quota.number_of_hosts + assert host_a.destroy + assert_equal host_b.name, quota.missing_hosts.keys.first + assert host_b.destroy + assert_equal 0, quota.number_of_hosts + end + end + describe 'resource quota capacity' do - def setup - @host = FactoryBot.create(:host, :with_resource_quota) - @quota = @host.resource_quota - Setting[:resource_quota_global_no_action] = false - Setting[:resource_quota_optional_assignment] = false - User.current.resource_quota_is_optional = false + def validation_error_message_host(resource, exceeding, max) + /Validation failed: Resource quota Host exceeds #{resource} limit of \ +'[\w\s]+'-quota by #{exceeding} \(max\. #{max}\)/ + end + + def validation_error_message_quota(resource, exceeding, max) + /Validation failed: Resource quota #{resource} limit of '[\w\s]+'-quota is \ +already exceeded by #{exceeding} without adding the new host \(max\. #{max}\)/ end test 'should fail at determine utilization' do - stub_quota_utilization({}, { 'my.missing.host': [:cpu_cores] }) # fail on quota utilization + stub_quota_missing_hosts({ 'my.missing.host': [:cpu_cores] }) # fail on quota utilization stub_host_utilization({ cpu_cores: 5 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(cpu_cores: 10) - assert_not host.save - assert_includes host.errors.full_messages, # TODO: Determine why the error fails - "Resource quota Resource Quota '#{host.resource_quota.name}' cannot determine resources for 1 hosts." + quota = FactoryBot.create(:resource_quota, cpu_cores: 10) + error = assert_raises(ActiveRecord::RecordInvalid) do + FactoryBot.create(:host, resource_quota: quota) + end + + assert_match(/Resource quota Resource Quota '[\w\s]+' cannot determine resources for 1 hosts./, error.message) end test 'should fail at determine host resources' do - stub_quota_utilization({ cpu_cores: 5 }, {}) # pass quota utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { cpu_cores: 10 }, cpu_cores: 5) stub_host_utilization({}, { 'my.missing.host': [:cpu_cores] }) # fail on host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(cpu_cores: 10) + error = assert_raises(ActiveRecord::RecordInvalid) do + FactoryBot.create(:host, hostname: 'my.missing.host', resource_quota: quota) + end - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota Cannot determine host resources for #{host.name}" + assert_match(/Validation failed: Resource quota Cannot determine host resources for [\w\s]+/, error.message) end test 'should fail due to new host at verify limits (CPU cores)' do - stub_quota_utilization({ cpu_cores: 5 }, {}) # pass quota utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { cpu_cores: 5 }, cpu_cores: 10) stub_host_utilization({ cpu_cores: 10 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(cpu_cores: 10) + error = assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host, resource_quota: quota) } - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota Host exceeds CPU cores limit of '#{host.resource_quota.name}'-quota " \ - 'by 5 cores (max. 10 cores)' + assert_match(validation_error_message_host('CPU cores', '5 cores', '10 cores'), error.message) end test 'should fail due to new host at verify limits (disk space)' do - stub_quota_utilization({ disk_gb: 5 }, {}) # pass quota utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { disk_gb: 5 }, disk_gb: 10) stub_host_utilization({ disk_gb: 10 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(disk_gb: 10) + error = assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host, resource_quota: quota) } - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota Host exceeds Disk space limit of '#{host.resource_quota.name}'-quota " \ - 'by 5 GB (max. 10 GB)' + assert_match(validation_error_message_host('Disk space', '5 GB', '10 GB'), error.message) end test 'should fail due to new host at verify limits (memory)' do - stub_quota_utilization({ memory_mb: 5 * 1024 }, {}) # pass quota utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { memory_mb: 5 * 1024 }, memory_mb: 10 * 1024) stub_host_utilization({ memory_mb: 10 * 1024 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(memory_mb: 10 * 1024) + error = assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host, resource_quota: quota) } - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota Host exceeds Memory limit of '#{host.resource_quota.name}'-quota " \ - 'by 5 GB (max. 10 GB)' + assert_match(validation_error_message_host('Memory', '5 GB', '10 GB'), error.message) end test 'should fail due to quota utilization at verify limits (CPU cores)' do - stub_quota_utilization({ cpu_cores: 15 }, {}) # pass quota utilization - stub_host_utilization({ cpu_cores: 10 }, {}) # pass host utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { cpu_cores: 10 }, cpu_cores: 5) + stub_host_utilization({ cpu_cores: 5 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(cpu_cores: 10) + error = assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host, resource_quota: quota) } - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota CPU cores limit of '#{host.resource_quota.name}'-quota " \ - 'is already exceeded by 5 cores without adding the new host (max. 10 cores)' + assert_match(validation_error_message_quota('CPU cores', '5 cores', '5 cores'), error.message) end test 'should fail due to quota utilization at verify limits (disk space)' do - stub_quota_utilization({ disk_gb: 15 }, {}) # pass quota utilization - stub_host_utilization({ disk_gb: 10 }, {}) # pass host utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { disk_gb: 10 }, disk_gb: 5) + stub_host_utilization({ disk_gb: 5 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(disk_gb: 10) + error = assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host, resource_quota: quota) } - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota Disk space limit of '#{host.resource_quota.name}'-quota " \ - 'is already exceeded by 5 GB without adding the new host (max. 10 GB)' + assert_match(validation_error_message_quota('Disk space', '5 GB', '5 GB'), error.message) end test 'should fail due to quota utilization at verify limits (memory)' do - stub_quota_utilization({ memory_mb: 15 * 1024 }, {}) # pass quota utilization - stub_host_utilization({ memory_mb: 10 * 1024 }, {}) # pass host utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { memory_mb: 10 * 1024 }, memory_mb: 5 * 1024) + stub_host_utilization({ memory_mb: 5 * 1024 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(memory_mb: 10 * 1024) + error = assert_raises(ActiveRecord::RecordInvalid) { FactoryBot.create(:host, resource_quota: quota) } - assert_not host.save - assert_includes host.errors.full_messages, - "Resource quota Memory limit of '#{host.resource_quota.name}'-quota " \ - 'is already exceeded by 5 GB without adding the new host (max. 10 GB)' + assert_match(validation_error_message_quota('Memory', '5 GB', '5 GB'), error.message) end test 'should validate single host capacity' do - stub_quota_utilization({ memory_mb: 0 }, {}) # pass quota utilization stub_host_utilization({ memory_mb: 10 * 1024 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(memory_mb: 10 * 1024) + quota = FactoryBot.create(:resource_quota, memory_mb: 20 * 1024) + host = FactoryBot.create(:host, resource_quota: quota) + quota.reload assert host.save - # TODO: Test must be adapted, when host resources are added to resource quota - # assert_equal 10 * 1024, host.resource_quota.utilization[:memory_mb] - assert_nil host.resource_quota.utilization[:cpu_cores] - assert_equal 0, host.resource_quota.utilization[:memory_mb] - assert_nil host.resource_quota.utilization[:disk_gb] + assert_equal(10 * 1024, quota.utilization[:memory_mb]) end test 'should validate multi limit capacity (host only)' do - stub_quota_utilization({ cpu_cores: 0, memory_mb: 0, disk_gb: 0 }, {}) # pass quota utilization stub_host_utilization({ cpu_cores: 5, memory_mb: 10 * 1024, disk_gb: 0 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(cpu_cores: 20) - host.resource_quota.update!(memory_mb: 20 * 1024) - host.resource_quota.update!(disk_gb: 50) + quota = FactoryBot.create(:resource_quota, cpu_cores: 20, memory_mb: 20 * 1024, disk_gb: 50) + host = FactoryBot.create(:host, resource_quota: quota) + quota.reload assert host.save - # TODO: Test must be adapted, when host resources are added to resource quota - # assert_equal 5, host.resource_quota.utilization[:cpu_cores] - # assert_equal 10 * 1024, host.resource_quota.utilization[:memory_mb] - # assert_equal 0, host.resource_quota.utilization[:disk_gb] - assert_equal 0, host.resource_quota.utilization[:cpu_cores] - assert_equal 0, host.resource_quota.utilization[:memory_mb] - assert_equal 0, host.resource_quota.utilization[:disk_gb] + assert_equal({ cpu_cores: 5, memory_mb: 10 * 1024, disk_gb: 0 }, quota.utilization) end test 'should validate multi limit capacity (with quota utilization)' do - stub_quota_utilization({ cpu_cores: 5, memory_mb: 5 * 1024, disk_gb: 10 }, {}) # pass quota utilization + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { + cpu_cores: 5, + memory_mb: 5 * 1024, + disk_gb: 10, + }, + cpu_cores: 20, + memory_mb: 20 * 1024, + disk_gb: 50) + stub_host_utilization({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, {}) # pass host utilization + + host = FactoryBot.create(:host, resource_quota: quota) + quota.reload + + assert host.save + assert_equal({ cpu_cores: 7, memory_mb: 9 * 1024, disk_gb: 30 }, quota.utilization) + end + + test 'should remove host capacity from quota utilization' do + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { + cpu_cores: 5, + memory_mb: 5 * 1024, + disk_gb: 10, + }, + cpu_cores: 20, + memory_mb: 20 * 1024, + disk_gb: 50) stub_host_utilization({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, {}) # pass host utilization - host = FactoryBot.create(:host, :with_resource_quota) - host.resource_quota.update!(cpu_cores: 20) - host.resource_quota.update!(memory_mb: 20 * 1024) - host.resource_quota.update!(disk_gb: 50) + host = FactoryBot.create(:host, resource_quota: quota) + quota.reload assert host.save - # TODO: Test must be adapted, when host resources are added to resource quota - # assert_equal 7, host.resource_quota.utilization[:cpu_cores] - # assert_equal 9 * 1024, host.resource_quota.utilization[:memory_mb] - # assert_equal 30, host.resource_quota.utilization[:disk_gb] - assert_equal 5, host.resource_quota.utilization[:cpu_cores] - assert_equal 5 * 1024, host.resource_quota.utilization[:memory_mb] - assert_equal 10, host.resource_quota.utilization[:disk_gb] + assert_equal({ cpu_cores: 7, memory_mb: 9 * 1024, disk_gb: 30 }, quota.utilization) + + host.destroy! + quota.reload + assert_equal({ cpu_cores: 5, memory_mb: 5 * 1024, disk_gb: 10 }, quota.utilization) + end + + test 'should add host capacity of two hosts to quota utilization' do + quota = FactoryBot.create(:resource_quota, :with_existing_host_resources, + host_resources: { + cpu_cores: 5, + memory_mb: 5 * 1024, + disk_gb: 10, + }, + cpu_cores: 20, + memory_mb: 20 * 1024, + disk_gb: 50) + stub_host_utilization({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, {}) # pass host utilization + + host_a = FactoryBot.create(:host, resource_quota: quota) + host_b = FactoryBot.create(:host, resource_quota: quota) + quota.reload + + assert host_a.save + assert host_b.save + assert_equal({ cpu_cores: 9, memory_mb: 13 * 1024, disk_gb: 50 }, quota.utilization) + + host_a.destroy! + quota.reload + assert_equal({ cpu_cores: 7, memory_mb: 9 * 1024, disk_gb: 30 }, quota.utilization) + end + + test 'should re-associate host capacity when changing resource quota' do + stub_host_utilization({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, {}) # pass host utilization + + quota_a = FactoryBot.create(:resource_quota, + cpu_cores: 20, + memory_mb: 20 * 1024, + disk_gb: 50) + quota_b = FactoryBot.create(:resource_quota, + cpu_cores: 20, + memory_mb: 20 * 1024, + disk_gb: 50) + host = FactoryBot.create(:host, resource_quota: quota_a) + quota_a.reload + + assert_equal({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, quota_a.utilization) + assert_equal({ cpu_cores: 0, memory_mb: 0, disk_gb: 0 }, quota_b.utilization) + + host.resource_quota = quota_b + host.save + quota_a.reload + quota_b.reload + + assert_equal({ cpu_cores: 0, memory_mb: 0, disk_gb: 0 }, quota_a.utilization) + assert_equal({ cpu_cores: 2, memory_mb: 4 * 1024, disk_gb: 20 }, quota_b.utilization) end end end diff --git a/test/models/resource_quota_test.rb b/test/models/resource_quota_test.rb index d99532c..36e3191 100644 --- a/test/models/resource_quota_test.rb +++ b/test/models/resource_quota_test.rb @@ -16,49 +16,43 @@ def setup test 'hosts relation' do @quota.hosts << @host - as_admin { @quota.save! } assert_equal @host.id, @quota.hosts[0].id - assert_equal @quota.id, @host.resource_quota.id + assert_equal @quota.id, @host.reload.resource_quota.id end test 'users relation' do @quota.users << @user - as_admin { @quota.save! } assert_equal @user.id, @quota.users[0].id assert_equal @quota.id, @user.resource_quotas[0].id end test 'users relation delete user' do @quota.users << @user - as_admin { @quota.save! } assert_equal @user.id, @quota.users[0].id assert_equal @quota.id, @user.resource_quotas[0].id - as_admin { @user.destroy! } + @user.destroy assert_empty @quota.reload.users end test 'users relation delete quota' do @user.resource_quotas << @quota - as_admin { @user.save! } assert_equal @quota.users[0].id, @user.id assert_equal @user.resource_quotas[0].id, @quota.id - as_admin { @quota.destroy! } + @quota.destroy assert_empty @user.reload.resource_quotas end test 'usergroups relation' do @quota.usergroups << @usergroup - as_admin { @quota.save! } assert_equal @usergroup.id, @quota.usergroups[0].id assert_equal @quota.id, @usergroup.resource_quotas[0].id end test 'usergroup delete' do @quota.usergroups << @usergroup - as_admin { @quota.save! } assert_equal @usergroup.id, @quota.usergroups[0].id assert_equal @quota.id, @usergroup.resource_quotas[0].id - as_admin { @usergroup.destroy! } + @usergroup.destroy assert_empty @quota.reload.usergroups end @@ -83,50 +77,62 @@ def setup assert_equal 3, @quota.number_of_usergroups end + test 'utilization is empty' do + @quota.cpu_cores = 50 + exp_utilization = { cpu_cores: 0, memory_mb: nil, disk_gb: nil } + + assert_equal exp_utilization, @quota.utilization + end + + test 'utilization is nil' do + exp_utilization = { cpu_cores: nil, memory_mb: nil, disk_gb: nil } + + assert_equal exp_utilization, @quota.utilization + end + test 'utilization is set (cpu_cores)' do - @quota.utilization_cpu_cores = 13 + @quota.cpu_cores = 50 + @quota.hosts << @host + @quota.update_hosts_resources({ @host.name => { cpu_cores: 13 } }) + assert_equal 13, @quota.utilization[:cpu_cores] end test 'utilization is set (memory_mb)' do - @quota.utilization_memory_mb = 14 + @quota.memory_mb = 50 + @quota.hosts << @host + @quota.update_hosts_resources({ @host.name => { memory_mb: 14 } }) + assert_equal 14, @quota.utilization[:memory_mb] end test 'utilization is set (disk_gb)' do - @quota.utilization_disk_gb = 15 + @quota.disk_gb = 50 + @quota.hosts << @host + @quota.update_hosts_resources({ @host.name => { disk_gb: 15 } }) + assert_equal 15, @quota.utilization[:disk_gb] end test 'utilization is set (all parameters)' do exp_utilization = { cpu_cores: 3, memory_mb: 4, disk_gb: 5 } - @quota.utilization_cpu_cores = exp_utilization[:cpu_cores] - @quota.utilization_memory_mb = exp_utilization[:memory_mb] - @quota.utilization_disk_gb = exp_utilization[:disk_gb] - assert_equal exp_utilization, @quota.utilization - end + @quota.update(cpu_cores: 50, memory_mb: 50, disk_gb: 50) + @quota.hosts << @host + @quota.update_hosts_resources({ @host.name => exp_utilization }) - test 'utilization_ is set by utilization' do - exp_utilization = { cpu_cores: 6, memory_mb: 7, disk_gb: 8 } - @quota.utilization = exp_utilization assert_equal exp_utilization, @quota.utilization end - test 'utilization sets attributes' do - second_usergroup = FactoryBot.create :usergroup - third_usergroup = FactoryBot.create :usergroup - @quota.usergroups << [@usergroup, second_usergroup, third_usergroup] - assert_equal 3, @quota.number_of_usergroups - end - test 'determine utilization' do exp_utilization = { cpu_cores: 1, memory_mb: 1, disk_gb: 2 } exp_missing_hosts = {} + host_utilization = { + @host.name => exp_utilization, + } @quota.hosts << @host @quota.update(cpu_cores: 10, memory_mb: 10, disk_gb: 10) - as_admin { @quota.save! } - @quota.stub(:call_utilization_helper, [exp_utilization, exp_missing_hosts]) do + @quota.stub(:call_utilization_helper, [host_utilization, exp_missing_hosts]) do @quota.determine_utilization end assert_equal exp_utilization, @quota.utilization @@ -137,90 +143,72 @@ def setup host_a = FactoryBot.create :host host_b = FactoryBot.create :host exp_utilization = { cpu_cores: 1, memory_mb: 1, disk_gb: 2 } + host_utilization = { + host_a.name => { memory_mb: 1, disk_gb: 1 }, + host_b.name => { cpu_cores: 1, disk_gb: 1 }, + } exp_missing_hosts = { host_a.name => [:cpu_cores], host_b.name => [:memory_mb] } - @quota.hosts << [host_a, host_b] - @quota.update(cpu_cores: 10, memory_mb: 10, disk_gb: 10) - as_admin { @quota.save! } - - @quota.stub(:call_utilization_helper, [exp_utilization, exp_missing_hosts]) do - @quota.determine_utilization - end - assert_equal exp_utilization, @quota.utilization - assert_equal exp_missing_hosts, @quota.missing_hosts - end - - test 'utilization uses quota utilization_ fields' do - exp_utilization = { cpu_cores: 1, memory_mb: 1, disk_gb: 2 } - @quota.utilization_cpu_cores = exp_utilization[:cpu_cores] - @quota.utilization_memory_mb = exp_utilization[:memory_mb] - @quota.utilization_disk_gb = exp_utilization[:disk_gb] - - assert_equal exp_utilization, @quota.utilization - end - test 'missing_hosts are constructed' do - host_a = FactoryBot.create :host - host_b = FactoryBot.create :host - exp_utilization = { cpu_cores: 1, memory_mb: 1, disk_gb: 2 } - exp_missing_hosts = { host_a.name => [:cpu_cores], host_b.name => [:memory_mb] } @quota.hosts << [host_a, host_b] @quota.update(cpu_cores: 10, memory_mb: 10, disk_gb: 10) - as_admin { @quota.save! } - @quota.stub(:call_utilization_helper, [exp_utilization, exp_missing_hosts]) do + @quota.stub(:call_utilization_helper, [host_utilization, exp_missing_hosts]) do @quota.determine_utilization end - @quota.reload + assert_equal exp_utilization, @quota.utilization assert_equal exp_missing_hosts, @quota.missing_hosts - assert_equal 2, @quota.resource_quotas_missing_hosts.size - assert_equal host_a.id, @quota.resource_quotas_missing_hosts.find_by(missing_host_id: host_a.id).missing_host_id - assert_equal host_b.id, @quota.resource_quotas_missing_hosts.find_by(missing_host_id: host_b.id).missing_host_id - assert_equal host_a.resource_quota_missing_resources.resource_quota.id, @quota.id end test 'missing_hosts are destroyed on host destroy' do host_a = FactoryBot.create :host host_b = FactoryBot.create :host - exp_utilization = { cpu_cores: 1, memory_mb: 1, disk_gb: 2 } + host_utilization = { + host_a.name => { memory_mb: 1, disk_gb: 1 }, + host_b.name => { cpu_cores: 1, disk_gb: 1 }, + } exp_missing_hosts = { host_a.name => [:cpu_cores], host_b.name => [:memory_mb] } @quota.hosts << [host_a, host_b] @quota.update(cpu_cores: 10, memory_mb: 10, disk_gb: 10) - as_admin { @quota.save! } + @quota.save! - @quota.stub(:call_utilization_helper, [exp_utilization, exp_missing_hosts]) do + @quota.stub(:call_utilization_helper, [host_utilization, exp_missing_hosts]) do @quota.determine_utilization end - assert_equal 2, @quota.resource_quotas_missing_hosts.size - host_a.destroy! - @quota.reload - assert_equal 1, @quota.resource_quotas_missing_hosts.size - assert_equal host_b.id, @quota.resource_quotas_missing_hosts[0].missing_host.id - host_b.destroy! - @quota.reload - assert_equal 0, @quota.resource_quotas_missing_hosts.size + assert_equal @quota.number_of_missing_hosts, @quota.number_of_hosts + host_a.destroy + assert_equal 1, @quota.number_of_missing_hosts + assert_equal host_b.name, @quota.missing_hosts.keys.first + host_b.destroy + assert_equal 0, @quota.number_of_missing_hosts end test 'missing_hosts are destroyed on re-computing utilization' do + @quota.update(cpu_cores: 10, memory_mb: 10) host_a = FactoryBot.create :host host_b = FactoryBot.create :host - exp_utilization = { cpu_cores: 1, memory_mb: 1, disk_gb: 2 } - exp_missing_hosts_two = { host_a.name => [:cpu_cores], host_b.name => [:memory_mb] } - exp_missing_hosts_one = { host_b.name => [:memory_mb] } + host_utilization_two = { + host_a.name => { cpu_cores: 1, memory_mb: nil }, + host_b.name => { cpu_cores: nil, memory_mb: 1 }, + } + host_utilization_one = { + host_a.name => { cpu_cores: 1, memory_mb: 1 }, + host_b.name => { cpu_cores: nil, memory_mb: 1 }, + } + exp_missing_hosts_two = { host_a.name => [:memory_mb], host_b.name => [:cpu_cores] } + exp_missing_hosts_one = { host_b.name => [:cpu_cores] } + @quota.hosts << [host_a, host_b] - @quota.update(cpu_cores: 10, memory_mb: 10, disk_gb: 10) - as_admin { @quota.save! } - @quota.stub(:call_utilization_helper, [exp_utilization, exp_missing_hosts_two]) do + @quota.stub(:call_utilization_helper, [host_utilization_two, exp_missing_hosts_two]) do @quota.determine_utilization end - assert_equal 2, @quota.resource_quotas_missing_hosts.size - @quota.stub(:call_utilization_helper, [exp_utilization, exp_missing_hosts_one]) do + assert_equal 2, @quota.number_of_missing_hosts + @quota.stub(:call_utilization_helper, [host_utilization_one, exp_missing_hosts_one]) do @quota.determine_utilization end @quota.reload - assert_equal 1, @quota.resource_quotas_missing_hosts.size - assert_equal host_b.id, @quota.resource_quotas_missing_hosts - .find_by(missing_host_id: host_b.id).missing_host_id + assert_equal 1, @quota.number_of_missing_hosts + assert_equal [host_b.name], @quota.missing_hosts.keys end end end diff --git a/test/test_plugin_helper.rb b/test/test_plugin_helper.rb index 83a97a6..7b692ee 100644 --- a/test/test_plugin_helper.rb +++ b/test/test_plugin_helper.rb @@ -30,20 +30,16 @@ def assert_quota_equal(expexted_list, quota) end end -def stub_quota_utilization(return_utilization, return_missing_hosts) - ForemanResourceQuota::ResourceQuota.any_instance.stubs(:call_utilization_helper) - .returns([return_utilization, return_missing_hosts]) +def stub_quota_utilization(return_hosts_utilization) + ForemanResourceQuota::ResourceQuota.any_instance.stubs(:utilization) + .returns(return_hosts_utilization) +end + +def stub_quota_missing_hosts(return_missing_hosts) ForemanResourceQuota::ResourceQuota.any_instance.stubs(:missing_hosts) .returns(return_missing_hosts) - ForemanResourceQuota::ResourceQuota.any_instance.stubs(:missing_hosts=) - .returns - ForemanResourceQuota::ResourceQuota.any_instance.stubs(:utilization) - .returns(return_utilization) - ForemanResourceQuota::ResourceQuota.any_instance.stubs(:utilization=) - .returns end def stub_host_utilization(return_utilization, return_missing_hosts) - Host::Managed.any_instance.stubs(:call_utilization_helper) - .returns([return_utilization, return_missing_hosts]) + Host::Managed.any_instance.stubs(:call_utilization_helper).returns([return_utilization, return_missing_hosts]) end