Skip to content

Commit

Permalink
Add utilization and missing_hosts database integration
Browse files Browse the repository at this point in the history
Enable a ResourceQuota to track persistently which host resources were
missing during the last calculation of its utilization.

Extend the database with fields for utilization_<resource> and
missing_hosts. Add a one-to-many relation between Hosts and
ResourceQuotas, as a ResourceQuota can have several Hosts whose
resources could not be determined.

* Add ResourceQuotaMissingHost model
* Exclude ResourceQuota from Metrics/ClassLength
* Add api/v2/id/missing_hosts endpoint
* Add controller tests for missings_hosts and utilization
* Add model tests for missing_hosts and utilization
* Move generic stub functions to test_plugin_helper

Co-authored-by: Nadja Heitmann <[email protected]>
  • Loading branch information
bastian-src and nadjaheitmann committed Apr 30, 2024
1 parent 5145b65 commit 8cd6466
Show file tree
Hide file tree
Showing 16 changed files with 344 additions and 47 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Layout/ArgumentAlignment:
Gemspec/RequiredRubyVersion:
Enabled: false

Metrics/ClassLength:
Exclude:
- 'app/models/foreman_resource_quota/resource_quota.rb'

Metrics/MethodLength:
Enabled: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ResourceQuotasController < ::Api::V2::BaseController
end

before_action :find_resource, only: %i[show update destroy]
before_action :custom_find_resource, only: %i[utilization hosts users usergroups]
before_action :custom_find_resource, only: %i[utilization missing_hosts hosts users usergroups]

api :GET, '/resource_quotas', N_('List all resource quotas')
param_group :search_and_pagination, ::Api::V2::BaseController
Expand All @@ -35,6 +35,14 @@ def utilization
process_response @resource_quota
end

api :GET, '/resource_quotas/:id/missing_hosts',
N_('Show resources could not be determined when calculating utilization')
param :id, :identifier, required: true
def missing_hosts
@resource_quota.determine_utilization
process_response @resource_quota
end

api :GET, '/resource_quotas/:id/hosts', N_('Show hosts of a resource quota')
param :id, :identifier, required: true
def hosts
Expand Down
8 changes: 2 additions & 6 deletions app/helpers/foreman_resource_quota/resource_quota_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,8 @@ def utilization_from_resource_origins(resources, hosts, custom_resource_origins:
# { <host name>: [<list of to be determined resources>] }
# for example:
# {
# "host_a": {
# [ :cpu_cores, :disk_gb ]
# },
# "host_b": {
# [ :cpu_cores, :disk_gb ]
# },
# "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]).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module HostManagedExtensions
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
scoped_search relation: :resource_quota, on: :name, complete_value: true, rename: :resource_quota
end

Expand Down
103 changes: 86 additions & 17 deletions app/models/foreman_resource_quota/resource_quota.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module ForemanResourceQuota
class ResourceQuota < ApplicationRecord
include ResourceQuotaHelper
include Exceptions
include Authorizable
include Parameterizable::ByIdName
extend FriendlyId
Expand All @@ -12,20 +13,19 @@ class ResourceQuota < ApplicationRecord
self.table_name = 'resource_quotas'

has_many :resource_quotas_users, class_name: 'ResourceQuotaUser', inverse_of: :resource_quota, dependent: :destroy
has_many :users, class_name: '::User', through: :resource_quotas_users
has_many :resource_quotas_usergroups, class_name: 'ResourceQuotaUsergroup', inverse_of: :resource_quota,
dependent: :destroy
has_many :usergroups, class_name: '::Usergroup', through: :resource_quotas_usergroups
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 :users, class_name: '::User', through: :resource_quotas_users
has_many :usergroups, class_name: '::Usergroup', through: :resource_quotas_usergroups

validates :name, presence: true, uniqueness: true

scoped_search on: :name, complete_value: true
scoped_search on: :id, complete_enabled: false, only_explicit: true, validator: ScopedSearch::Validators::INTEGER

attribute :utilization, :jsonb, default: {}
attribute :missing_hosts, :jsonb, default: {}

def number_of_hosts
hosts.size
end
Expand All @@ -38,13 +38,68 @@ def number_of_usergroups
usergroups.size
end

def number_of_missing_hosts
missing_hosts.size
end

# Returns a Hash with host name as key and a list of missing resources as value
# { <host name>: [<list of missing resources>] }
# for example:
# {
# "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
end
missing_hosts_list
end

# Set the hosts that are listed in resource_quotas_missing_hosts
# Parameters:
# - val: Hash of host names and list of missing resources
# { <host name>: [<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)
end
end

def utilization
{
cpu_cores: utilization_cpu_cores,
memory_mb: utilization_memory_mb,
disk_gb: utilization_disk_gb,
}
end

def utilization=(val)
update_single_utilization(:cpu_cores, val)
update_single_utilization(:memory_mb, val)
update_single_utilization(:disk_gb, val)
end

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

print_warning(missing_hosts, quota_hosts) unless missing_hosts.empty?
quota_utilization, missing_hosts_resources = call_utilization_helper(quota_hosts)
update(utilization: quota_utilization)
update(missing_hosts: missing_hosts_resources)
Rails.logger.warn create_hosts_resources_warning(missing_hosts_resources) unless missing_hosts.empty?
rescue StandardError => e
print_error(e) # print error log here and forward error
Rails.logger.error("An error occured while determining resources for quota '#{name}': #{e}")
raise e
end

Expand All @@ -67,17 +122,31 @@ 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 #{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" unless missing_host.nil?
def create_hosts_resources_warning(missing_hosts_resources)
warn_text = +"Could not determines resources for #{missing_hosts_resources.size} hosts:"
missing_hosts_resources.each do |host_name, missing_resources|
warn_text << " '#{host_name}': '#{missing_resources}'\n" unless missing_resources.empty?
end
Rails.logger.warn warn_text
end

def print_error(err)
Rails.logger.error("An error occured while determining resources for quota '#{name}': #{err}")
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
10 changes: 10 additions & 0 deletions app/models/foreman_resource_quota/resource_quota_missing_host.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
object @resource_quota

attributes :name, :id, :description, :cpu_cores, :memory_mb, :disk_gb, :number_of_hosts, :number_of_users,
:number_of_usergroups
:number_of_usergroups, :number_of_missing_hosts, :utilization
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

object @resource_quota

extends 'api/v2/resource_quotas/main'

attributes :missing_hosts
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
end
constraints(id: %r{[^/]+}) do
get 'utilization'
get 'missing_hosts'
get 'hosts'
get 'users'
get 'usergroups'
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20230306120001_create_resource_quotas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ def change
t.integer :cpu_cores, default: nil
t.integer :memory_mb, default: nil
t.integer :disk_gb, default: nil
t.integer :utilization_cpu_cores, default: nil
t.integer :utilization_memory_mb, default: nil
t.integer :utilization_disk_gb, default: nil

t.timestamps
end
Expand All @@ -24,6 +27,16 @@ def change
t.belongs_to :user
t.timestamps
end

create_table :resource_quotas_missing_hosts do |t|
t.references :resource_quota, null: false, foreign_key: { to_table: :resource_quotas }
t.references :missing_host, null: false, unique: true, foreign_key: { to_table: :hosts }
t.boolean :no_cpu_cores, default: false
t.boolean :no_memory_mb, default: false
t.boolean :no_disk_gb, default: false
t.timestamps
end

add_reference :hosts, :resource_quota, foreign_key: { to_table: :resource_quotas }
add_column :users, :resource_quota_is_optional, :boolean, default: false
end
Expand Down
1 change: 1 addition & 0 deletions lib/foreman_resource_quota/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ class HostResourceQuotaEmptyException < ResourceQuotaException; end
class ResourceLimitException < ResourceQuotaException; end
class HostResourcesException < ResourceQuotaException; end
class ResourceQuotaUtilizationException < ResourceQuotaException; end
class HostNotFoundException < ResourceQuotaException; end
end
end
5 changes: 3 additions & 2 deletions lib/foreman_resource_quota/register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
security_block :foreman_resource_quota do
permission 'view_foreman_resource_quota/resource_quotas',
{ 'foreman_resource_quota/resource_quotas': %i[index welcome auto_complete_search],
'foreman_resource_quota/api/v2/resource_quotas': %i[index show utilization hosts users usergroups
'foreman_resource_quota/api/v2/resource_quotas': %i[index show utilization missing_hosts hosts users usergroups
auto_complete_search],
'foreman_resource_quota/api/v2/resource_quotas/:resource_quota_id/': %i[utilization hosts users usergroups] },
'foreman_resource_quota/api/v2/resource_quotas/:resource_quota_id/': %i[utilization missing_hosts hosts users
usergroups] },
resource_type: 'ForemanResourceQuota::ResourceQuota'
permission 'create_foreman_resource_quota/resource_quotas',
{ 'foreman_resource_quota/resource_quotas': %i[new create],
Expand Down
30 changes: 24 additions & 6 deletions test/controllers/api/v2/resource_quotas_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ def setup
as_admin { @quota.save! }
end

def add_quota
quota = FactoryBot.create :resource_quota
as_admin { @quota.save! }
quota
end

test 'should get index with quotas' do
get :index, session: set_session_user
assert_response :success
Expand Down Expand Up @@ -126,6 +120,30 @@ def add_quota
assert_response :not_found
assert_equal nof_quota_before, ResourceQuota.all.size
end

test 'should show utilization' do
exp_utilization = { cpu_cores: 10, memory_mb: 20 }
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)
assert_not show_response.empty?
assert_equal @quota.id, show_response['id']
assert_equal exp_utilization, show_response['utilization'].transform_keys(&:to_sym)
end

test 'should show missing_hosts' do
exp_missing_hosts = { 'some_host' => %i[cpu_cores memory_mb] }
stub_quota_utilization({}, 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)
assert_not show_response.empty?
assert_equal @quota.id, show_response['id']
# JSON.decode makes everything strings -> convert 'some_host' value to symbols:
assert_equal(exp_missing_hosts,
show_response['missing_hosts'].transform_values { |value| value.map(&:to_sym) })
end
end
end
end
Expand Down
22 changes: 9 additions & 13 deletions test/models/concerns/host_managed_extension_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,9 @@ def setup
User.current.resource_quota_is_optional = false
end

def stub_quota_utilization(return_utilization, return_missing_hosts)
ResourceQuota.any_instance.stubs(:call_utilization_helper)
.returns([return_utilization, return_missing_hosts])
end

def stub_host_utilization(return_utilization, return_missing_hosts)
Host::Managed.any_instance.stubs(:call_utilization_helper)
.returns([return_utilization, return_missing_hosts])
end

test 'should fail at determine utilization' do
stub_quota_utilization({}, { '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)

Expand Down Expand Up @@ -168,9 +157,10 @@ def stub_host_utilization(return_utilization, return_missing_hosts)

assert host.save
# TODO: Test must be adapted, when host resources are added to resource quota
# assert_equal nil, host.resource_quota.utilization[:cpu_cores]
# assert_equal 10 * 1024, host.resource_quota.utilization[:memory_mb]
# assert_equal nil, host.resource_quota.utilization[:disk_gb]
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]
end

test 'should validate multi limit capacity (host only)' do
Expand All @@ -187,6 +177,9 @@ def stub_host_utilization(return_utilization, return_missing_hosts)
# 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]
end

test 'should validate multi limit capacity (with quota utilization)' do
Expand All @@ -203,6 +196,9 @@ def stub_host_utilization(return_utilization, return_missing_hosts)
# 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]
end
end
end
Expand Down
Loading

0 comments on commit 8cd6466

Please sign in to comment.