foreman_resource_quota 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +0 -1
  3. data/app/helpers/foreman_resource_quota/resource_quota_helper.rb +31 -3
  4. data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +77 -34
  5. data/app/models/foreman_resource_quota/host_resources.rb +43 -0
  6. data/app/models/foreman_resource_quota/resource_quota.rb +73 -59
  7. data/app/models/foreman_resource_quota/resource_quota_host.rb +10 -0
  8. data/app/services/foreman_resource_quota/resource_origin.rb +4 -4
  9. data/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +6 -6
  10. data/config/initializers/inflections.rb +2 -0
  11. data/db/migrate/20240611141744_remove_utilization_from_resource_quotas.rb +9 -0
  12. data/db/migrate/20240611141939_drop_missing_hosts.rb +7 -0
  13. data/db/migrate/20240611142813_create_hosts_resources.rb +21 -0
  14. data/db/migrate/20240618163434_remove_resource_quota_from_hosts.rb +7 -0
  15. data/lib/foreman_resource_quota/async/refresh_resource_quota_utilization.rb +25 -0
  16. data/lib/foreman_resource_quota/engine.rb +43 -9
  17. data/lib/foreman_resource_quota/register.rb +17 -17
  18. data/lib/foreman_resource_quota/version.rb +1 -1
  19. data/package.json +2 -2
  20. data/webpack/components/ResourceQuotaForm/components/Properties/index.js +6 -4
  21. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +1 -1
  22. metadata +45 -5
  23. data/app/models/foreman_resource_quota/resource_quota_missing_host.rb +0 -10
  24. /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: ebffd9d19b92459f29ae9d7838ab0293afb7a02d8873e7175f4074c357e412f2
4
- data.tar.gz: d77e360fad5da830c368a020cffeb52f3986b1cbfd1cc9c4c54b8963c034c82e
3
+ metadata.gz: e5ae7058a06490ad3f9c4b8a299834a4c5cf013cabfd0a6123bf9ba4d26c3c47
4
+ data.tar.gz: ddc85109fc571d9d723181765dcb81b8302acfb721556fcd1fd9739af234baf9
5
5
  SHA512:
6
- metadata.gz: c3949a4112d516b2ca0798f002909983271d1020559e303f0935b3a17837bc70a294d9c3d9388efdefac4f60671f8a6dbe27d953c74d816066c4966b1a9a13fe
7
- data.tar.gz: be8e5f021cbd0ab79cb6a1ecd61677c51292d97408f21d085a35b46c3538081f4d6aa5be71153fdb8bd0fbcbab860d6253c77b871d175cad8b36dd9e4156e3bf
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
- utilization_sum = resources.each.with_object({}) { |key, hash| hash[key] = 0 }
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
- utilization_sum,
71
+ hosts_resources,
63
72
  missing_hosts_resources,
64
73
  hosts_hash
65
74
  )
66
75
  end
67
76
 
68
- [utilization_sum, missing_hosts_resources]
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 :check_resource_quota_capacity
11
-
12
- belongs_to :resource_quota, class_name: '::ForemanResourceQuota::ResourceQuota'
13
- has_one :resource_quota_missing_resources, class_name: '::ForemanResourceQuota::ResourceQuotaMissingHost',
14
- inverse_of: :missing_host, foreign_key: :missing_host_id, dependent: :destroy
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 check_resource_quota_capacity
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
- host_resources = determine_host_resources
41
- verify_resource_quota_limits(quota_utilization, host_resources)
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
- resource_quota.determine_utilization
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 '#{resource_quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
75
+ "Resource Quota '#{quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
56
76
  end
57
- resource_quota.utilization
77
+ quota.utilization(exclude: [name])
58
78
  end
59
79
 
60
- def determine_host_resources
61
- (host_resources, missing_hosts) = call_utilization_helper(resource_quota.active_resources, [self])
62
- unless missing_hosts.empty?
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 verify_resource_quota_limits(quota_utilization, host_resources)
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 = resource_quota[resource_type]
74
- all_hosts_utilization = resource_utilization + host_resources[resource_type.to_sym]
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 < max_quota
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
- resource_quota.name,
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
- resource_quota.name,
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 early_return?
99
- if resource_quota.nil?
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 resource_quota.active_resources.empty?
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 :resource_quotas_missing_hosts, class_name: 'ResourceQuotaMissingHost', inverse_of: :resource_quota,
19
- dependent: :destroy
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
- hosts.size
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
- def missing_hosts
53
- # Initialize default value as an empty array
54
- missing_hosts_list = Hash.new { |hash, key| hash[key] = [] }
55
- resource_quotas_missing_hosts.each do |missing_host_rel|
56
- host_name = missing_host_rel.missing_host.name
57
- missing_hosts_list[host_name] << :cpu_cores if missing_host_rel.no_cpu_cores
58
- missing_hosts_list[host_name] << :memory_mb if missing_host_rel.no_memory_mb
59
- missing_hosts_list[host_name] << :disk_gb if missing_host_rel.no_disk_gb
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
- missing_hosts_list
68
+ missing_hosts
62
69
  end
63
70
 
64
- # Set the hosts that are listed in resource_quotas_missing_hosts
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
- # - val: Hash of host names and list of missing resources
67
- # { <host name>: [<list of missing resources>] }
68
- # for example:
69
- # {
70
- # "host_a": [ :cpu_cores, :disk_gb ],
71
- # "host_b": [ :memory_mb ],
72
- # }
73
- def missing_hosts=(val)
74
- # Delete all entries and write new ones
75
- resource_quotas_missing_hosts.delete_all
76
- val.each do |host_name, missing_resources|
77
- add_missing_host(host_name, missing_resources)
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 utilization
82
- {
83
- cpu_cores: utilization_cpu_cores,
84
- memory_mb: utilization_memory_mb,
85
- disk_gb: utilization_disk_gb,
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 utilization=(val)
90
- update_single_utilization(:cpu_cores, val)
91
- update_single_utilization(:memory_mb, val)
92
- update_single_utilization(:disk_gb, val)
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
- quota_utilization, missing_hosts_resources = call_utilization_helper(quota_hosts)
98
- update(utilization: quota_utilization)
99
- update(missing_hosts: missing_hosts_resources)
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!(resources_sum, missing_hosts_resources, host_objects)
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
- sum_resources_and_delete_missing_hosts!(resources_sum, missing_hosts_resources, host_values)
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 sum_resources_and_delete_missing_hosts!(resources_sum, missing_hosts_resources, host_values)
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
- resources_sum[resource_name] += resource_value
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!(resources_sum, missing_hosts_resources, _host_objects)
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!(resources_sum, missing_hosts_resources, host.name, 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
- # - resources_sum: Hash containing total resources sum.
66
+ # - hosts_resources: Hash containing successfully determined resources per host.
67
67
  # - missing_hosts_resources: Hash containing missing resources per host.
68
- # - host_name: Name of the host.
68
+ # - host: Host object
69
69
  # - vm: Compute resource VM object of given host.
70
70
  # Returns: None.
71
- def process_host_vm!(resources_sum, missing_hosts_resources, host_name, 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
- resources_sum[resource_name] += resource_value
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?
@@ -2,4 +2,6 @@
2
2
 
3
3
  ActiveSupport::Inflector.inflections do |inflect|
4
4
  inflect.irregular 'resource_quota', 'resource_quotas'
5
+ inflect.irregular 'host_resources', 'hosts_resources'
6
+ inflect.irregular 'HostResources', 'HostsResources'
5
7
  end
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DropMissingHosts < ActiveRecord::Migration[6.1]
4
+ def up
5
+ drop_table :resource_quotas_missing_hosts
6
+ end
7
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveResourceQuotaFromHosts < ActiveRecord::Migration[6.1]
4
+ def change
5
+ remove_reference :hosts, :resource_quota, foreign_key: true
6
+ end
7
+ 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 |_app|
38
- require 'foreman_resource_quota/register'
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.0'
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 'view_foreman_resource_quota/resource_quotas',
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 'create_foreman_resource_quota/resource_quotas',
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 'edit_foreman_resource_quota/resource_quotas',
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 'destroy_foreman_resource_quota/resource_quotas',
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', ['view_foreman_resource_quota/resource_quotas',
37
- 'create_foreman_resource_quota/resource_quotas',
38
- 'edit_foreman_resource_quota/resource_quotas',
39
- 'destroy_foreman_resource_quota/resource_quotas',
40
- 'view_hosts',
41
- 'edit_hosts',
42
- 'view_users',
43
- 'edit_users']
44
- role 'Resource Quota User', ['view_foreman_resource_quota/resource_quotas',
45
- 'view_hosts',
46
- 'view_users',
47
- 'view_usergroups']
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanResourceQuota
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
data/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {},
27
27
  "devDependencies": {
28
- "@babel/core": "^7.7.0",
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.4.0",
40
+ "stylelint": "^16.10.0",
41
41
  "stylelint-config-standard": "^36.0.0"
42
42
  }
43
43
  }
@@ -16,10 +16,12 @@ import {
16
16
  Tooltip,
17
17
  } from '@patternfly/react-core';
18
18
 
19
- import UserIcon from '@patternfly/react-icons/dist/esm/icons/user-icon';
20
- import UsersIcon from '@patternfly/react-icons/dist/esm/icons/users-icon';
21
- import ClusterIcon from '@patternfly/react-icons/dist/esm/icons/cluster-icon';
22
- import SyncAltIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
19
+ import {
20
+ UserIcon,
21
+ UsersIcon,
22
+ ClusterIcon,
23
+ SyncAltIcon,
24
+ } from '@patternfly/react-icons';
23
25
 
24
26
  import { translate as __ } from 'foremanReact/common/I18n';
25
27
  import { dispatchAPICallbackToast } from '../../../../api_helper';
@@ -7,7 +7,7 @@ import {
7
7
  ProgressMeasureLocation,
8
8
  Tooltip,
9
9
  } from '@patternfly/react-core';
10
- import SyncAltIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon';
10
+ import SyncAltIcon from '@patternfly/react-icons';
11
11
 
12
12
  import { translate as __ } from 'foremanReact/common/I18n';
13
13
 
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.2.1
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-05-09 00:00:00.000000000 Z
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/resource_quota_missing_host.rb
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