foreman_resource_quota 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +0 -1
- data/app/helpers/foreman_resource_quota/resource_quota_helper.rb +31 -3
- data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +77 -34
- data/app/models/foreman_resource_quota/host_resources.rb +43 -0
- data/app/models/foreman_resource_quota/resource_quota.rb +73 -59
- data/app/models/foreman_resource_quota/resource_quota_host.rb +10 -0
- data/app/services/foreman_resource_quota/resource_origin.rb +4 -4
- data/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +6 -6
- data/config/initializers/inflections.rb +2 -0
- data/db/migrate/20240611141744_remove_utilization_from_resource_quotas.rb +9 -0
- data/db/migrate/20240611141939_drop_missing_hosts.rb +7 -0
- data/db/migrate/20240611142813_create_hosts_resources.rb +21 -0
- data/db/migrate/20240618163434_remove_resource_quota_from_hosts.rb +7 -0
- data/lib/foreman_resource_quota/async/refresh_resource_quota_utilization.rb +25 -0
- data/lib/foreman_resource_quota/engine.rb +43 -9
- data/lib/foreman_resource_quota/register.rb +17 -17
- data/lib/foreman_resource_quota/version.rb +1 -1
- data/package.json +2 -2
- metadata +45 -5
- data/app/models/foreman_resource_quota/resource_quota_missing_host.rb +0 -10
- /data/{lib → app/lib}/foreman_resource_quota/exceptions.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5ae7058a06490ad3f9c4b8a299834a4c5cf013cabfd0a6123bf9ba4d26c3c47
|
4
|
+
data.tar.gz: ddc85109fc571d9d723181765dcb81b8302acfb721556fcd1fd9739af234baf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88f0f162d137fe245f38733a9f361624d81f36d889d114877fe70654b67d0de8f31daf3916aa029da9d9cf5f30ebc19b8152f565f24709e565fe74edd6ed98a1
|
7
|
+
data.tar.gz: 1bef610395fd2a5901623c9370d70339b8b12f53bd7d7f96aefe80d70607daa73c1b731b88ca2a73578219ee2cf59786fa8d3147ff0b092e9cc7e6209faa9230
|
@@ -64,7 +64,6 @@ module ForemanResourceQuota
|
|
64
64
|
def_param_group :resource_quota do
|
65
65
|
param :resource_quota, Hash, required: true, action_aware: true do
|
66
66
|
param :name, String, required: true
|
67
|
-
# param :operatingsystem_ids, Array, :desc => N_("Operating system IDs")
|
68
67
|
end
|
69
68
|
end
|
70
69
|
|
@@ -52,20 +52,29 @@ module ForemanResourceQuota
|
|
52
52
|
format(format_text, unit_applied_value, symbol)
|
53
53
|
end
|
54
54
|
|
55
|
+
# Use different resource origins to determine host resource utilization.
|
56
|
+
# - iterates all given hosts and tries do determine their resources utilization
|
57
|
+
# Returns:
|
58
|
+
# [ <hosts_resources>, <missing_hosts_resources> ]
|
59
|
+
# for example:
|
60
|
+
# [
|
61
|
+
# { "host_a": { cpu_cores: 20, memory_mb: 8196 }, "host_b": { cpu_cores: 15, memory_mb: nil } },
|
62
|
+
# { "host_c": [ :memory_mb ] },
|
63
|
+
# ]
|
55
64
|
def utilization_from_resource_origins(resources, hosts, custom_resource_origins: nil)
|
56
|
-
|
65
|
+
hosts_resources = create_hosts_resources_hash(hosts, resources)
|
57
66
|
missing_hosts_resources = create_missing_hosts_resources_hash(hosts, resources)
|
58
67
|
hosts_hash = hosts.index_by(&:name)
|
59
68
|
resource_classes = custom_resource_origins || default_resource_origin_classes
|
60
69
|
resource_classes.each do |origin_class|
|
61
70
|
origin_class.new.collect_resources!(
|
62
|
-
|
71
|
+
hosts_resources,
|
63
72
|
missing_hosts_resources,
|
64
73
|
hosts_hash
|
65
74
|
)
|
66
75
|
end
|
67
76
|
|
68
|
-
[
|
77
|
+
[hosts_resources, missing_hosts_resources]
|
69
78
|
end
|
70
79
|
|
71
80
|
private
|
@@ -89,6 +98,25 @@ module ForemanResourceQuota
|
|
89
98
|
hosts.map(&:name).index_with { resources_to_determine.clone }
|
90
99
|
end
|
91
100
|
|
101
|
+
# Create a Hash that maps resources and a value to host names.
|
102
|
+
# { <host name>: {<hash of resource values>} }
|
103
|
+
# for example:
|
104
|
+
# {
|
105
|
+
# "host_a": { cpu_cores: nil, disk_gb: nil },
|
106
|
+
# "host_b": { cpu_cores: nil, disk_gb: nil },
|
107
|
+
# }
|
108
|
+
# Parameters:
|
109
|
+
# - hosts: Array of host objects.
|
110
|
+
# - resources: Array of resources (as symbol, e.g. [:cpu_cores, :disk_gb]).
|
111
|
+
# Returns: Hash with host names as keys and resource-hashs as values.
|
112
|
+
def create_hosts_resources_hash(hosts, resources)
|
113
|
+
return {} if hosts.empty? || resources.empty?
|
114
|
+
|
115
|
+
# Create a hash template with resources mapped to nil
|
116
|
+
resources_to_determine = resources.index_with { |_resource| nil }
|
117
|
+
hosts.map(&:name).index_with { resources_to_determine.dup }
|
118
|
+
end
|
119
|
+
|
92
120
|
# Default classes that are used to determine host resources. Determines
|
93
121
|
# resources in the order of this list.
|
94
122
|
def default_resource_origin_classes
|
@@ -7,16 +7,23 @@ module ForemanResourceQuota
|
|
7
7
|
include ForemanResourceQuota::Exceptions
|
8
8
|
|
9
9
|
included do
|
10
|
-
validate :
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
validate :verify_resource_quota
|
11
|
+
|
12
|
+
has_one :host_resources, class_name: '::ForemanResourceQuota::HostResources',
|
13
|
+
inverse_of: :host, foreign_key: :host_id, dependent: :destroy
|
14
|
+
has_one :resource_quota_host, class_name: '::ForemanResourceQuota::ResourceQuotaHost',
|
15
|
+
inverse_of: :host, foreign_key: :host_id, dependent: :destroy
|
16
|
+
has_one :resource_quota, class_name: '::ForemanResourceQuota::ResourceQuota',
|
17
|
+
through: :resource_quota_host
|
15
18
|
scoped_search relation: :resource_quota, on: :name, complete_value: true, rename: :resource_quota
|
19
|
+
|
20
|
+
# A host shall always have a .host_resources attribute
|
21
|
+
before_validation :build_host_resources, unless: -> { host_resources.present? }
|
22
|
+
after_save :save_host_resources, if: -> { host_resources.changed? }
|
16
23
|
end
|
17
24
|
|
18
|
-
def
|
19
|
-
handle_quota_check
|
25
|
+
def verify_resource_quota
|
26
|
+
handle_quota_check(resource_quota)
|
20
27
|
true
|
21
28
|
rescue ResourceQuotaException => e
|
22
29
|
handle_error('resource_quota_id',
|
@@ -32,13 +39,27 @@ module ForemanResourceQuota
|
|
32
39
|
format('An unknown error occured while checking the resource quota capacity: %s', e))
|
33
40
|
end
|
34
41
|
|
42
|
+
def resource_quota_id
|
43
|
+
resource_quota&.id
|
44
|
+
end
|
45
|
+
|
46
|
+
def resource_quota_id=(val)
|
47
|
+
if val.blank?
|
48
|
+
resource_quota_host&.destroy
|
49
|
+
else
|
50
|
+
quota = ForemanResourceQuota::ResourceQuota.find_by(id: val)
|
51
|
+
raise ActiveRecord::RecordNotFound, "ResourceQuota with ID \"#{val}\" not found" unless quota
|
52
|
+
self.resource_quota = quota
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
35
56
|
private
|
36
57
|
|
37
|
-
def handle_quota_check
|
38
|
-
return if early_return?
|
39
|
-
quota_utilization = determine_quota_utilization
|
40
|
-
|
41
|
-
|
58
|
+
def handle_quota_check(quota)
|
59
|
+
return if early_return?(quota)
|
60
|
+
quota_utilization = determine_quota_utilization(quota)
|
61
|
+
current_host_resources = determine_host_resources(quota.active_resources)
|
62
|
+
check_resource_quota_limits(quota, quota_utilization, current_host_resources)
|
42
63
|
end
|
43
64
|
|
44
65
|
def handle_error(error_module, error_message, log_message)
|
@@ -47,60 +68,72 @@ module ForemanResourceQuota
|
|
47
68
|
false
|
48
69
|
end
|
49
70
|
|
50
|
-
def determine_quota_utilization
|
51
|
-
|
52
|
-
missing_hosts = resource_quota.missing_hosts
|
71
|
+
def determine_quota_utilization(quota)
|
72
|
+
missing_hosts = quota.missing_hosts(exclude: [name])
|
53
73
|
unless missing_hosts.empty?
|
54
74
|
raise ResourceQuotaUtilizationException,
|
55
|
-
"Resource Quota '#{
|
75
|
+
"Resource Quota '#{quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
|
56
76
|
end
|
57
|
-
|
77
|
+
quota.utilization(exclude: [name])
|
58
78
|
end
|
59
79
|
|
60
|
-
def determine_host_resources
|
61
|
-
|
62
|
-
|
80
|
+
def determine_host_resources(active_resources)
|
81
|
+
new_host_resources, missing_hosts = call_utilization_helper(active_resources, [self])
|
82
|
+
if missing_hosts.key?(name) || missing_hosts.key?(name.to_sym)
|
63
83
|
raise HostResourcesException,
|
64
|
-
"Cannot determine host resources for #{name}"
|
84
|
+
"Cannot determine host resources for #{name}: #{missing_hosts[name]}"
|
65
85
|
end
|
66
|
-
host_resources
|
86
|
+
host_resources.resources = new_host_resources
|
87
|
+
host_resources.resources
|
67
88
|
end
|
68
89
|
|
69
|
-
def
|
90
|
+
def check_resource_quota_limits(quota, quota_utilization, current_host_resources)
|
70
91
|
quota_utilization.each do |resource_type, resource_utilization|
|
71
92
|
next if resource_utilization.nil?
|
72
93
|
|
73
|
-
max_quota =
|
74
|
-
all_hosts_utilization = resource_utilization +
|
94
|
+
max_quota = quota[resource_type]
|
95
|
+
all_hosts_utilization = resource_utilization + current_host_resources[resource_type.to_sym]
|
75
96
|
next if all_hosts_utilization <= max_quota
|
76
97
|
|
77
|
-
raise ResourceLimitException, formulate_limit_error(resource_utilization,
|
98
|
+
raise ResourceLimitException, formulate_limit_error(quota.name, resource_utilization,
|
78
99
|
all_hosts_utilization, max_quota, resource_type)
|
79
100
|
end
|
80
101
|
end
|
81
102
|
|
82
|
-
def formulate_limit_error(resource_utilization, all_hosts_utilization, max_quota, resource_type)
|
83
|
-
if resource_utilization
|
103
|
+
def formulate_limit_error(quota_name, resource_utilization, all_hosts_utilization, max_quota, resource_type)
|
104
|
+
if resource_utilization <= max_quota
|
84
105
|
N_(format("Host exceeds %s limit of '%s'-quota by %s (max. %s)",
|
85
106
|
natural_resource_name_by_type(resource_type),
|
86
|
-
|
107
|
+
quota_name,
|
87
108
|
resource_value_to_string(all_hosts_utilization - max_quota, resource_type),
|
88
109
|
resource_value_to_string(max_quota, resource_type)))
|
89
110
|
else
|
90
111
|
N_(format("%s limit of '%s'-quota is already exceeded by %s without adding the new host (max. %s)",
|
91
112
|
natural_resource_name_by_type(resource_type),
|
92
|
-
|
113
|
+
quota_name,
|
93
114
|
resource_value_to_string(resource_utilization - max_quota, resource_type),
|
94
115
|
resource_value_to_string(max_quota, resource_type)))
|
95
116
|
end
|
96
117
|
end
|
97
118
|
|
98
|
-
def
|
99
|
-
|
119
|
+
def formulate_resource_inconsistency_error(quota_name, resource_type, quota_utilization_value, resource_value)
|
120
|
+
N_("Resource Quota '#{quota_name}' inconsistency detected while destroying host '#{name}':\n" \
|
121
|
+
"Resource Quota #{resource_type} current utilization: #{quota_utilization_value}.\n" \
|
122
|
+
"Host resource value: #{resource_value}.\n" \
|
123
|
+
'Skipping.')
|
124
|
+
end
|
125
|
+
|
126
|
+
def formulate_quota_inconsistency_error(quota_name)
|
127
|
+
N_("An error occured adapting the resource quota utilization of '#{quota_name}' " \
|
128
|
+
"while processing host '#{name}'. The resource quota utilization values might be inconsistent.")
|
129
|
+
end
|
130
|
+
|
131
|
+
def early_return?(quota)
|
132
|
+
if quota.nil?
|
100
133
|
return true if quota_assigment_optional?
|
101
134
|
raise HostResourceQuotaEmptyException, 'must be given.'
|
102
135
|
end
|
103
|
-
return true if
|
136
|
+
return true if quota.active_resources.empty?
|
104
137
|
return true if Setting[:resource_quota_global_no_action] # quota is assigned, but not supposed to be checked
|
105
138
|
false
|
106
139
|
end
|
@@ -109,9 +142,19 @@ module ForemanResourceQuota
|
|
109
142
|
owner.resource_quota_is_optional || Setting[:resource_quota_optional_assignment]
|
110
143
|
end
|
111
144
|
|
145
|
+
def save_host_resources
|
146
|
+
host_resources.save
|
147
|
+
end
|
148
|
+
|
112
149
|
# Wrap into a function for easier testing
|
113
150
|
def call_utilization_helper(resources, hosts)
|
114
|
-
utilization_from_resource_origins(resources, hosts)
|
151
|
+
all_host_resources, missing_hosts = utilization_from_resource_origins(resources, hosts)
|
152
|
+
unless all_host_resources.key?(name)
|
153
|
+
raise HostResourcesException,
|
154
|
+
"Host #{name} was not included when determining host resources."
|
155
|
+
end
|
156
|
+
current_host_resources = all_host_resources[name]
|
157
|
+
[current_host_resources, missing_hosts]
|
115
158
|
end
|
116
159
|
end
|
117
160
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
class HostResources < ApplicationRecord
|
5
|
+
self.table_name = 'hosts_resources'
|
6
|
+
|
7
|
+
belongs_to :host, class_name: '::Host::Managed'
|
8
|
+
validates :host, { presence: true, uniqueness: true }
|
9
|
+
|
10
|
+
def resources
|
11
|
+
{
|
12
|
+
cpu_cores: cpu_cores,
|
13
|
+
memory_mb: memory_mb,
|
14
|
+
disk_gb: disk_gb,
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def resources=(val)
|
19
|
+
allowed_attributes = val.slice(:cpu_cores, :memory_mb, :disk_gb)
|
20
|
+
assign_attributes(allowed_attributes) # Set multiple attributes at once (given a hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns an array of unknown host resources (returns an empty array if all are known)
|
24
|
+
# For example, completely unknown host resources returns:
|
25
|
+
# [
|
26
|
+
# :cpu_cores,
|
27
|
+
# :memory_mb,
|
28
|
+
# :disk_gb,
|
29
|
+
# ]
|
30
|
+
# Consider only the resource_quota's active resources by default.
|
31
|
+
def missing_resources(only_active_resources: true)
|
32
|
+
empty_resources = []
|
33
|
+
resources_to_check = %i[cpu_cores memory_mb disk_gb]
|
34
|
+
resources_to_check = host.resource_quota.active_resources if only_active_resources && host.resource_quota.present?
|
35
|
+
|
36
|
+
resources_to_check.each do |single_resource|
|
37
|
+
empty_resources << single_resource if send(single_resource).nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
empty_resources
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -12,12 +12,12 @@ module ForemanResourceQuota
|
|
12
12
|
|
13
13
|
self.table_name = 'resource_quotas'
|
14
14
|
|
15
|
+
has_many :resource_quotas_hosts, class_name: 'ResourceQuotaHost', inverse_of: :resource_quota, dependent: :destroy
|
15
16
|
has_many :resource_quotas_users, class_name: 'ResourceQuotaUser', inverse_of: :resource_quota, dependent: :destroy
|
16
17
|
has_many :resource_quotas_usergroups, class_name: 'ResourceQuotaUsergroup', inverse_of: :resource_quota,
|
17
18
|
dependent: :destroy
|
18
|
-
has_many :
|
19
|
-
|
20
|
-
has_many :hosts, class_name: '::Host::Managed', dependent: :nullify
|
19
|
+
has_many :hosts, -> { distinct }, class_name: '::Host::Managed', through: :resource_quotas_hosts
|
20
|
+
has_many :hosts_resources, class_name: 'HostResources', through: :hosts
|
21
21
|
has_many :users, class_name: '::User', through: :resource_quotas_users
|
22
22
|
has_many :usergroups, class_name: '::Usergroup', through: :resource_quotas_usergroups
|
23
23
|
|
@@ -26,8 +26,12 @@ module ForemanResourceQuota
|
|
26
26
|
scoped_search on: :name, complete_value: true
|
27
27
|
scoped_search on: :id, complete_enabled: false, only_explicit: true, validator: ScopedSearch::Validators::INTEGER
|
28
28
|
|
29
|
+
def self.permission_name
|
30
|
+
'resource_quotas'
|
31
|
+
end
|
32
|
+
|
29
33
|
def number_of_hosts
|
30
|
-
|
34
|
+
hosts_resources.size
|
31
35
|
end
|
32
36
|
|
33
37
|
def number_of_users
|
@@ -49,54 +53,84 @@ module ForemanResourceQuota
|
|
49
53
|
# "host_a": [ :cpu_cores, :disk_gb ],
|
50
54
|
# "host_b": [ :memory_mb ],
|
51
55
|
# }
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
# Parameters:
|
57
|
+
# - exclude: an Array of host names to exclude from the utilization
|
58
|
+
def missing_hosts(exclude: [])
|
59
|
+
missing_hosts = {}
|
60
|
+
active_resources.each do |single_resource|
|
61
|
+
hosts_resources.where(single_resource => nil).includes([:host]).find_each do |host_resources_item|
|
62
|
+
host_name = host_resources_item.host.name
|
63
|
+
next if exclude.include?(host_name)
|
64
|
+
missing_hosts[host_name] ||= []
|
65
|
+
missing_hosts[host_name] << single_resource
|
66
|
+
end
|
60
67
|
end
|
61
|
-
|
68
|
+
missing_hosts
|
62
69
|
end
|
63
70
|
|
64
|
-
#
|
71
|
+
# Returns a Hash with the quota resources and their utilization as key-value pair
|
72
|
+
# It returns always all resources, even if they are not used (nil in that case).
|
73
|
+
# For example:
|
74
|
+
# {
|
75
|
+
# cpu_cores: 10,
|
76
|
+
# memory_mb: nil,
|
77
|
+
# disk_gb: 20,
|
78
|
+
# }
|
65
79
|
# Parameters:
|
66
|
-
# -
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
80
|
+
# - exclude: an Array of host names to exclude from the utilization
|
81
|
+
def utilization(exclude: [])
|
82
|
+
current_utilization = {
|
83
|
+
cpu_cores: nil,
|
84
|
+
memory_mb: nil,
|
85
|
+
disk_gb: nil,
|
86
|
+
}
|
87
|
+
|
88
|
+
active_resources.each do |resource|
|
89
|
+
current_utilization[resource] = 0
|
90
|
+
end
|
91
|
+
|
92
|
+
hosts_resources.each do |host_resources_item|
|
93
|
+
next if exclude.include?(host_resources_item.host.name)
|
94
|
+
|
95
|
+
active_resources.each do |resource|
|
96
|
+
current_utilization[resource] += host_resources_item.send(resource).to_i
|
97
|
+
end
|
78
98
|
end
|
99
|
+
|
100
|
+
current_utilization
|
79
101
|
end
|
80
102
|
|
81
|
-
def
|
82
|
-
{
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
103
|
+
def hosts_resources_as_hash
|
104
|
+
resources_hash = hosts.map(&:name).index_with { {} }
|
105
|
+
hosts_resources.each do |host_resources_item|
|
106
|
+
active_resources do |resource_name|
|
107
|
+
resources_hash[host_resources_item.host.name][resource_name] = host_resources_item.send(resource_name)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
resources_hash
|
87
111
|
end
|
88
112
|
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
|
113
|
+
def update_hosts_resources(hosts_resources_hash)
|
114
|
+
# Only update hosts that are associated with this quota
|
115
|
+
update_hosts = hosts.where(name: hosts_resources_hash.keys)
|
116
|
+
update_hosts_ids = update_hosts.pluck(:name, :id).to_h
|
117
|
+
hosts_resources_hash.each do |host_name, resources|
|
118
|
+
# Update the host_resources without loading the whole host object
|
119
|
+
host_resources_item = hosts_resources.find_by(host_id: update_hosts_ids[host_name])
|
120
|
+
if host_resources_item
|
121
|
+
host_resources_item.resources = resources
|
122
|
+
host_resources_item.save
|
123
|
+
else
|
124
|
+
Rails.logger.warn "HostResources not found for host_name: #{host_name}"
|
125
|
+
end
|
126
|
+
end
|
93
127
|
end
|
94
128
|
|
95
129
|
def determine_utilization(additional_hosts = [])
|
96
130
|
quota_hosts = (hosts | (additional_hosts))
|
97
|
-
|
98
|
-
|
99
|
-
|
131
|
+
all_host_resources, missing_hosts_resources = call_utilization_helper(quota_hosts)
|
132
|
+
update_hosts_resources(all_host_resources)
|
133
|
+
|
100
134
|
Rails.logger.warn create_hosts_resources_warning(missing_hosts_resources) unless missing_hosts.empty?
|
101
135
|
rescue StandardError => e
|
102
136
|
Rails.logger.error("An error occured while determining resources for quota '#{name}': #{e}")
|
@@ -128,25 +162,5 @@ module ForemanResourceQuota
|
|
128
162
|
warn_text << " '#{host_name}': '#{missing_resources}'\n" unless missing_resources.empty?
|
129
163
|
end
|
130
164
|
end
|
131
|
-
|
132
|
-
def update_single_utilization(attribute, val)
|
133
|
-
return unless val.key?(attribute.to_sym) || val.key?(attribute.to_s)
|
134
|
-
update("utilization_#{attribute}": val[attribute.to_sym] || val[attribute.to_s])
|
135
|
-
end
|
136
|
-
|
137
|
-
def add_missing_host(host_name, missing_resources)
|
138
|
-
return if missing_resources.empty?
|
139
|
-
|
140
|
-
host = Host::Managed.find_by(name: host_name)
|
141
|
-
raise HostNotFoundException if host.nil?
|
142
|
-
|
143
|
-
resource_quotas_missing_hosts << ResourceQuotaMissingHost.new(
|
144
|
-
missing_host: host,
|
145
|
-
resource_quota: self,
|
146
|
-
no_cpu_cores: missing_resources.include?(:cpu_cores),
|
147
|
-
no_memory_mb: missing_resources.include?(:memory_mb),
|
148
|
-
no_disk_gb: missing_resources.include?(:disk_gb)
|
149
|
-
)
|
150
|
-
end
|
151
165
|
end
|
152
166
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
class ResourceQuotaHost < ApplicationRecord
|
5
|
+
self.table_name = 'resource_quotas_hosts'
|
6
|
+
|
7
|
+
belongs_to :resource_quota, class_name: 'ResourceQuota'
|
8
|
+
belongs_to :host, class_name: '::Host::Managed'
|
9
|
+
end
|
10
|
+
end
|
@@ -8,12 +8,12 @@ module ForemanResourceQuota
|
|
8
8
|
disk_gb: :extract_disk_gb,
|
9
9
|
}.freeze
|
10
10
|
|
11
|
-
def collect_resources!(
|
11
|
+
def collect_resources!(hosts_resources, missing_hosts_resources, host_objects)
|
12
12
|
return if missing_hosts_resources.empty?
|
13
13
|
|
14
14
|
relevant_hosts = load_hosts_eagerly(missing_hosts_resources, host_objects, host_attribute_eager_name)
|
15
15
|
host_values = collect_attribute_from_hosts(relevant_hosts, host_attribute_name)
|
16
|
-
|
16
|
+
process_resources_and_delete_missing_hosts!(hosts_resources, missing_hosts_resources, host_values)
|
17
17
|
end
|
18
18
|
|
19
19
|
def host_attribute_eager_name
|
@@ -65,12 +65,12 @@ module ForemanResourceQuota
|
|
65
65
|
host_values
|
66
66
|
end
|
67
67
|
|
68
|
-
def
|
68
|
+
def process_resources_and_delete_missing_hosts!(hosts_resources, missing_hosts_resources, host_values)
|
69
69
|
host_values.each do |host_name, attribute_content|
|
70
70
|
missing_hosts_resources[host_name].reverse_each do |resource_name|
|
71
71
|
resource_value = process_resource(resource_name, attribute_content)
|
72
72
|
next unless resource_value
|
73
|
-
|
73
|
+
hosts_resources[host_name][resource_name] = resource_value
|
74
74
|
missing_hosts_resources[host_name].delete(resource_name)
|
75
75
|
end
|
76
76
|
missing_hosts_resources.delete(host_name) if missing_hosts_resources[host_name].empty?
|
@@ -20,7 +20,7 @@ module ForemanResourceQuota
|
|
20
20
|
nil
|
21
21
|
end
|
22
22
|
|
23
|
-
def collect_resources!(
|
23
|
+
def collect_resources!(hosts_resources, missing_hosts_resources, _host_objects)
|
24
24
|
compute_resource_to_hosts = group_hosts_by_compute_resource(missing_hosts_resources.keys)
|
25
25
|
|
26
26
|
compute_resource_to_hosts.each do |compute_resource_id, hosts|
|
@@ -32,7 +32,7 @@ module ForemanResourceQuota
|
|
32
32
|
hosts.each do |host|
|
33
33
|
vm = host_vms.find { |obj| obj.send(vm_id_attr) == host.uuid }
|
34
34
|
next unless vm
|
35
|
-
process_host_vm!(
|
35
|
+
process_host_vm!(hosts_resources, missing_hosts_resources, host.name, vm)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -63,16 +63,16 @@ module ForemanResourceQuota
|
|
63
63
|
|
64
64
|
# Processes a host's virtual machines and updates resource allocation.
|
65
65
|
# Parameters:
|
66
|
-
# -
|
66
|
+
# - hosts_resources: Hash containing successfully determined resources per host.
|
67
67
|
# - missing_hosts_resources: Hash containing missing resources per host.
|
68
|
-
# -
|
68
|
+
# - host: Host object
|
69
69
|
# - vm: Compute resource VM object of given host.
|
70
70
|
# Returns: None.
|
71
|
-
def process_host_vm!(
|
71
|
+
def process_host_vm!(hosts_resources, missing_hosts_resources, host_name, host_vm)
|
72
72
|
missing_hosts_resources[host_name].reverse_each do |resource_name|
|
73
73
|
resource_value = process_resource(resource_name, host_vm)
|
74
74
|
next unless resource_value
|
75
|
-
|
75
|
+
hosts_resources[host_name][resource_name] = resource_value
|
76
76
|
missing_hosts_resources[host_name].delete(resource_name)
|
77
77
|
end
|
78
78
|
missing_hosts_resources.delete(host_name) if missing_hosts_resources[host_name].empty?
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RemoveUtilizationFromResourceQuotas < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
remove_column :resource_quotas, :utilization_cpu_cores, :integer
|
6
|
+
remove_column :resource_quotas, :utilization_memory_mb, :integer
|
7
|
+
remove_column :resource_quotas, :utilization_disk_gb, :integer
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateHostsResources < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
create_table :hosts_resources do |t|
|
6
|
+
t.belongs_to :host, index: { unique: true }, foreign_key: true, null: false
|
7
|
+
t.integer :cpu_cores, default: nil
|
8
|
+
t.integer :memory_mb, default: nil
|
9
|
+
t.integer :disk_gb, default: nil
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table :resource_quotas_hosts do |t|
|
15
|
+
t.belongs_to :host, index: { unique: true }, foreign_key: true, null: false
|
16
|
+
t.belongs_to :resource_quota, foreign_key: true, null: false
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module Async
|
5
|
+
class RefreshResourceQuotaUtilization < ::Actions::EntryAction
|
6
|
+
include ::Actions::RecurringAction
|
7
|
+
|
8
|
+
def run
|
9
|
+
ResourceQuota.all.each do |quota|
|
10
|
+
quota.determine_utilization
|
11
|
+
rescue e
|
12
|
+
logger.error N_(format("An error occured determining the utilization of '%s'-quota: %s", quota.name, e))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
action_logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def rescue_strategy_for_self
|
21
|
+
Dynflow::Action::Rescue::Fail
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,16 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'foreman_tasks'
|
4
|
+
|
3
5
|
module ForemanResourceQuota
|
4
6
|
class Engine < ::Rails::Engine
|
5
7
|
engine_name 'foreman_resource_quota'
|
6
8
|
|
7
|
-
config.autoload_paths += Dir["#{config.root}/app/models/"]
|
8
|
-
config.autoload_paths += Dir["#{config.root}/app/controllers/"]
|
9
|
-
config.autoload_paths += Dir["#{config.root}/app/views/"]
|
10
|
-
config.autoload_paths += Dir["#{config.root}/app/services/foreman_resource_quota/"]
|
11
|
-
config.autoload_paths += Dir["#{config.root}/app/helpers/foreman_resource_quota/"]
|
12
|
-
config.autoload_paths += Dir["#{config.root}/lib/"]
|
13
|
-
|
14
9
|
# Add db migrations
|
15
10
|
initializer 'foreman_resource_quota.load_app_instance_data' do |app|
|
16
11
|
ForemanResourceQuota::Engine.paths['db/migrate'].existent.each do |path|
|
@@ -34,8 +29,10 @@ module ForemanResourceQuota
|
|
34
29
|
end
|
35
30
|
|
36
31
|
# Plugin extensions
|
37
|
-
initializer 'foreman_resource_quota.register_plugin', before: :finisher_hook do |
|
38
|
-
|
32
|
+
initializer 'foreman_resource_quota.register_plugin', before: :finisher_hook do |app|
|
33
|
+
app.reloader.to_prepare do
|
34
|
+
require 'foreman_resource_quota/register'
|
35
|
+
end
|
39
36
|
end
|
40
37
|
|
41
38
|
# Include concerns in this config.to_prepare block
|
@@ -47,10 +44,47 @@ module ForemanResourceQuota
|
|
47
44
|
Rails.logger.warn "ForemanResourceQuota: skipping engine hook (#{e})"
|
48
45
|
end
|
49
46
|
|
47
|
+
# Register ForemanTasks-based recurring logic/scheduled tasks
|
48
|
+
initializer 'foreman_resource_quota.register_scheduled_tasks', before: :finisher_hook do |_app|
|
49
|
+
action_paths = [ForemanResourceQuota::Engine.root.join('lib/foreman_resource_quota/async')]
|
50
|
+
::ForemanTasks.dynflow.config.eager_load_paths.concat(action_paths)
|
51
|
+
|
52
|
+
# Skip object creation if the admin user is not present
|
53
|
+
# skip database manipulations while tables do not exist, like in migrations
|
54
|
+
if ActiveRecord::Base.connection.data_source_exists?(::ForemanTasks::Task.table_name) &&
|
55
|
+
User.unscoped.find_by_login(User::ANONYMOUS_ADMIN).present?
|
56
|
+
# Register the scheduled tasks
|
57
|
+
::ForemanTasks.dynflow.config.on_init(false) do |_world|
|
58
|
+
ForemanResourceQuota::Engine.register_scheduled_task(
|
59
|
+
ForemanResourceQuota::Async::RefreshResourceQuotaUtilization,
|
60
|
+
'0 1 * * *'
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
rescue ActiveRecord::NoDatabaseError => e
|
65
|
+
Rails.logger.warn "ForemanResourceQuota: skipping ForemanTasks registration hook (#{e})"
|
66
|
+
end
|
67
|
+
|
50
68
|
initializer 'foreman_resource_quota.register_gettext', after: :load_config_initializers do |_app|
|
51
69
|
locale_dir = File.join(File.expand_path('../..', __dir__), 'locale')
|
52
70
|
locale_domain = 'foreman_resource_quota'
|
53
71
|
Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
|
54
72
|
end
|
73
|
+
|
74
|
+
# Helper to register ForemanTasks
|
75
|
+
def self.register_scheduled_task(task_class, cronline)
|
76
|
+
return if ::ForemanTasks::RecurringLogic.joins(:tasks)
|
77
|
+
.merge(::ForemanTasks::Task.where(label: task_class.name))
|
78
|
+
.exists?
|
79
|
+
::ForemanTasks::RecurringLogic.transaction(isolation: :serializable) do
|
80
|
+
User.as_anonymous_admin do
|
81
|
+
recurring_logic = ::ForemanTasks::RecurringLogic.new_from_cronline(cronline)
|
82
|
+
recurring_logic.save!
|
83
|
+
recurring_logic.start(task_class)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue ActiveRecord::TransactionIsolationError => e
|
87
|
+
Rails.logger.warn "ForemanResourceQuota: skipping RecurringLogic registration hook (#{e})"
|
88
|
+
end
|
55
89
|
end
|
56
90
|
end
|
@@ -2,29 +2,29 @@
|
|
2
2
|
|
3
3
|
# rubocop: disable Metrics/BlockLength
|
4
4
|
Foreman::Plugin.register :foreman_resource_quota do
|
5
|
-
requires_foreman '>= 3.
|
5
|
+
requires_foreman '>= 3.13'
|
6
6
|
# Apipie
|
7
7
|
apipie_documented_controllers ["#{ForemanResourceQuota::Engine.root}" \
|
8
8
|
'/app/controllers/foreman_resource_quot/api/v2/*.rb']
|
9
9
|
|
10
10
|
# Add permissions
|
11
11
|
security_block :foreman_resource_quota do
|
12
|
-
permission
|
12
|
+
permission :view_resource_quotas,
|
13
13
|
{ 'foreman_resource_quota/resource_quotas': %i[index welcome auto_complete_search],
|
14
14
|
'foreman_resource_quota/api/v2/resource_quotas': %i[index show utilization missing_hosts hosts users usergroups
|
15
15
|
auto_complete_search],
|
16
16
|
'foreman_resource_quota/api/v2/resource_quotas/:resource_quota_id/': %i[utilization missing_hosts hosts users
|
17
17
|
usergroups] },
|
18
18
|
resource_type: 'ForemanResourceQuota::ResourceQuota'
|
19
|
-
permission
|
19
|
+
permission :create_resource_quotas,
|
20
20
|
{ 'foreman_resource_quota/resource_quotas': %i[new create],
|
21
21
|
'foreman_resource_quota/api/v2/resource_quotas': %i[create] },
|
22
22
|
resource_type: 'ForemanResourceQuota::ResourceQuota'
|
23
|
-
permission
|
23
|
+
permission :edit_resource_quotas,
|
24
24
|
{ 'foreman_resource_quota/resource_quotas': %i[edit update],
|
25
25
|
'foreman_resource_quota/api/v2/resource_quotas': %i[update] },
|
26
26
|
resource_type: 'ForemanResourceQuota::ResourceQuota'
|
27
|
-
permission
|
27
|
+
permission :destroy_resource_quotas,
|
28
28
|
{ 'foreman_resource_quota/resource_quotas': %i[destroy],
|
29
29
|
'foreman_resource_quota/api/v2/resource_quotas': %i[destroy] },
|
30
30
|
resource_type: 'ForemanResourceQuota::ResourceQuota'
|
@@ -33,18 +33,18 @@ Foreman::Plugin.register :foreman_resource_quota do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Add a permissions to default roles (Viewer and Manager)
|
36
|
-
role 'Resource Quota Manager', [
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
role 'Resource Quota User', [
|
45
|
-
|
46
|
-
|
47
|
-
|
36
|
+
role 'Resource Quota Manager', %i[view_resource_quotas
|
37
|
+
create_resource_quotas
|
38
|
+
edit_resource_quotas
|
39
|
+
destroy_resource_quotas
|
40
|
+
view_hosts
|
41
|
+
edit_hosts
|
42
|
+
view_users
|
43
|
+
edit_users]
|
44
|
+
role 'Resource Quota User', %i[view_resource_quotas
|
45
|
+
view_hosts
|
46
|
+
view_users
|
47
|
+
view_usergroups]
|
48
48
|
add_all_permissions_to_default_roles
|
49
49
|
|
50
50
|
# add controller parameter extension
|
data/package.json
CHANGED
@@ -25,7 +25,7 @@
|
|
25
25
|
},
|
26
26
|
"dependencies": {},
|
27
27
|
"devDependencies": {
|
28
|
-
"@babel/core": "^7.
|
28
|
+
"@babel/core": "^7.26.0",
|
29
29
|
"@sheerun/mutationobserver-shim": "^0.3.3",
|
30
30
|
"@testing-library/react": "^10.4.9",
|
31
31
|
"@theforeman/builder": ">=12.0.1",
|
@@ -37,7 +37,7 @@
|
|
37
37
|
"eslint": "^6.7.2",
|
38
38
|
"prettier": "^1.19.1",
|
39
39
|
"react-redux-test-utils": "^0.2.0",
|
40
|
-
"stylelint": "^16.
|
40
|
+
"stylelint": "^16.10.0",
|
41
41
|
"stylelint-config-standard": "^36.0.0"
|
42
42
|
}
|
43
43
|
}
|
metadata
CHANGED
@@ -1,15 +1,49 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_resource_quota
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bastian Schmidt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-11-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: foreman-tasks
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '11'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '10.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '11'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: theforeman-rubocop
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.1.0
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.1.0
|
13
47
|
description: Foreman Plug-in to manage resource usage among users.
|
14
48
|
email:
|
15
49
|
- schmidt@atix.de
|
@@ -26,11 +60,13 @@ files:
|
|
26
60
|
- app/controllers/foreman_resource_quota/resource_quotas_controller.rb
|
27
61
|
- app/helpers/foreman_resource_quota/hosts_helper.rb
|
28
62
|
- app/helpers/foreman_resource_quota/resource_quota_helper.rb
|
63
|
+
- app/lib/foreman_resource_quota/exceptions.rb
|
29
64
|
- app/models/concerns/foreman_resource_quota/host_managed_extensions.rb
|
30
65
|
- app/models/concerns/foreman_resource_quota/user_extensions.rb
|
31
66
|
- app/models/concerns/foreman_resource_quota/usergroup_extensions.rb
|
67
|
+
- app/models/foreman_resource_quota/host_resources.rb
|
32
68
|
- app/models/foreman_resource_quota/resource_quota.rb
|
33
|
-
- app/models/foreman_resource_quota/
|
69
|
+
- app/models/foreman_resource_quota/resource_quota_host.rb
|
34
70
|
- app/models/foreman_resource_quota/resource_quota_user.rb
|
35
71
|
- app/models/foreman_resource_quota/resource_quota_usergroup.rb
|
36
72
|
- app/services/foreman_resource_quota/resource_origin.rb
|
@@ -62,9 +98,13 @@ files:
|
|
62
98
|
- config/initializers/inflections.rb
|
63
99
|
- config/routes.rb
|
64
100
|
- db/migrate/20230306120001_create_resource_quotas.rb
|
101
|
+
- db/migrate/20240611141744_remove_utilization_from_resource_quotas.rb
|
102
|
+
- db/migrate/20240611141939_drop_missing_hosts.rb
|
103
|
+
- db/migrate/20240611142813_create_hosts_resources.rb
|
104
|
+
- db/migrate/20240618163434_remove_resource_quota_from_hosts.rb
|
65
105
|
- lib/foreman_resource_quota.rb
|
106
|
+
- lib/foreman_resource_quota/async/refresh_resource_quota_utilization.rb
|
66
107
|
- lib/foreman_resource_quota/engine.rb
|
67
|
-
- lib/foreman_resource_quota/exceptions.rb
|
68
108
|
- lib/foreman_resource_quota/register.rb
|
69
109
|
- lib/foreman_resource_quota/version.rb
|
70
110
|
- lib/tasks/foreman_resource_quota_tasks.rake
|
@@ -1,10 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ForemanResourceQuota
|
4
|
-
class ResourceQuotaMissingHost < ApplicationRecord
|
5
|
-
self.table_name = 'resource_quotas_missing_hosts'
|
6
|
-
|
7
|
-
belongs_to :resource_quota, inverse_of: :resource_quotas_missing_hosts
|
8
|
-
belongs_to :missing_host, class_name: '::Host::Managed', inverse_of: :resource_quota_missing_resources
|
9
|
-
end
|
10
|
-
end
|
File without changes
|