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

Add missing hosts and utilization to database #40

Merged
merged 1 commit into from
Apr 30, 2024
Merged
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
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:"
bastian-src marked this conversation as resolved.
Show resolved Hide resolved
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
Loading