foreman_resource_quota 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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