foreman_resource_quota 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +51 -0
- data/Rakefile +49 -0
- data/app/controllers/concerns/foreman/controller/parameters/resource_quota.rb +28 -0
- data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +96 -0
- data/app/controllers/foreman_resource_quota/application_controller.rb +9 -0
- data/app/controllers/foreman_resource_quota/resource_quotas_controller.rb +50 -0
- data/app/helpers/foreman_resource_quota/hosts_helper.rb +18 -0
- data/app/helpers/foreman_resource_quota/resource_quota_helper.rb +107 -0
- data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +115 -0
- data/app/models/concerns/foreman_resource_quota/user_extensions.rb +15 -0
- data/app/models/concerns/foreman_resource_quota/usergroup_extensions.rb +14 -0
- data/app/models/foreman_resource_quota/resource_quota.rb +83 -0
- data/app/models/foreman_resource_quota/resource_quota_user.rb +10 -0
- data/app/models/foreman_resource_quota/resource_quota_usergroup.rb +10 -0
- data/app/services/foreman_resource_quota/resource_origin.rb +97 -0
- data/app/services/foreman_resource_quota/resource_origins/compute_attributes_origin.rb +64 -0
- data/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +82 -0
- data/app/services/foreman_resource_quota/resource_origins/facts_origin.rb +68 -0
- data/app/services/foreman_resource_quota/resource_origins/vm_attributes_origin.rb +40 -0
- data/app/views/foreman_resource_quota/api/v2/hosts/resource_quota.json.rabl +3 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/base.json.rabl +6 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/create.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/hosts.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/index.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/main.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/show.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/update.json.rabl +5 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/usergroups.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/users.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/resource_quotas/utilization.json.rabl +7 -0
- data/app/views/foreman_resource_quota/api/v2/usergroups/resource_quota.json.rabl +3 -0
- data/app/views/foreman_resource_quota/api/v2/users/resource_quota.json.rabl +3 -0
- data/app/views/foreman_resource_quota/resource_quotas/_form.html.erb +21 -0
- data/app/views/foreman_resource_quota/resource_quotas/edit.html.erb +12 -0
- data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +55 -0
- data/app/views/foreman_resource_quota/resource_quotas/new.html.erb +10 -0
- data/app/views/foreman_resource_quota/resource_quotas/welcome.html.erb +10 -0
- data/app/views/hosts/_form_quota_fields.html.erb +4 -0
- data/app/views/users/_form_quota_tab.html.erb +45 -0
- data/config/initializers/inflections.rb +5 -0
- data/config/routes.rb +43 -0
- data/db/migrate/20230306120001_create_resource_quotas.rb +31 -0
- data/lib/foreman_resource_quota/engine.rb +56 -0
- data/lib/foreman_resource_quota/exceptions.rb +11 -0
- data/lib/foreman_resource_quota/register.rb +106 -0
- data/lib/foreman_resource_quota/version.rb +5 -0
- data/lib/foreman_resource_quota.rb +6 -0
- data/lib/tasks/foreman_resource_quota_tasks.rake +50 -0
- data/locale/Makefile +60 -0
- data/locale/en/foreman_resource_quota.po +18 -0
- data/locale/foreman_resource_quota.pot +19 -0
- data/locale/gemspec.rb +4 -0
- data/package.json +44 -0
- data/webpack/api_helper.js +113 -0
- data/webpack/api_helper.test.js +96 -0
- data/webpack/components/CreateResourceQuotaModal.js +46 -0
- data/webpack/components/ResourceQuotaEmptyState/index.js +58 -0
- data/webpack/components/ResourceQuotaForm/ResourceQuotaForm.scss +1 -0
- data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +71 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/Properties.scss +9 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StaticDetail.js +72 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.js +71 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.test.js +50 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +131 -0
- data/webpack/components/ResourceQuotaForm/components/Properties/index.js +190 -0
- data/webpack/components/ResourceQuotaForm/components/QuotaState.js +157 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/Resource.scss +13 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +224 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +151 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.scss +10 -0
- data/webpack/components/ResourceQuotaForm/components/Resource/index.js +239 -0
- data/webpack/components/ResourceQuotaForm/components/Resources.js +105 -0
- data/webpack/components/ResourceQuotaForm/components/Submit.js +72 -0
- data/webpack/components/ResourceQuotaForm/index.js +185 -0
- data/webpack/components/UpdateResourceQuotaModal.js +143 -0
- data/webpack/global_index.js +15 -0
- data/webpack/global_test_setup.js +11 -0
- data/webpack/helper.js +86 -0
- data/webpack/index.js +23 -0
- data/webpack/lib/ActionableDetail.js +115 -0
- data/webpack/lib/ActionableDetail.scss +4 -0
- data/webpack/lib/EditableSwitch.js +47 -0
- data/webpack/lib/EditableTextInput/EditableTextInput.js +206 -0
- data/webpack/lib/EditableTextInput/PencilEditButton.js +27 -0
- data/webpack/lib/EditableTextInput/__tests__/editableTextInput.test.js +193 -0
- data/webpack/lib/EditableTextInput/editableTextInput.scss +38 -0
- data/webpack/lib/EditableTextInput/index.js +4 -0
- data/webpack/lib/react-testing-lib-wrapper.js +80 -0
- data/webpack/test_setup.js +17 -0
- metadata +134 -0
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'bundler/setup'
|
6
|
+
rescue LoadError
|
7
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
8
|
+
end
|
9
|
+
begin
|
10
|
+
require 'rdoc/task'
|
11
|
+
rescue LoadError
|
12
|
+
require 'rdoc/rdoc'
|
13
|
+
require 'rake/rdoctask'
|
14
|
+
RDoc::Task = Rake::RDocTask
|
15
|
+
end
|
16
|
+
|
17
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'ForemanResourceQuota'
|
20
|
+
rdoc.options << '--line-numbers'
|
21
|
+
rdoc.rdoc_files.include('README.rdoc')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
24
|
+
|
25
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
26
|
+
|
27
|
+
Bundler::GemHelper.install_tasks
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
|
31
|
+
Rake::TestTask.new(:test) do |t|
|
32
|
+
t.libs << 'lib'
|
33
|
+
t.libs << 'test'
|
34
|
+
t.pattern = 'test/**/*_test.rb'
|
35
|
+
t.verbose = false
|
36
|
+
end
|
37
|
+
|
38
|
+
task default: :test
|
39
|
+
|
40
|
+
begin
|
41
|
+
require 'rubocop/rake_task'
|
42
|
+
RuboCop::RakeTask.new
|
43
|
+
rescue StandardError => _e
|
44
|
+
puts 'Rubocop not loaded.'
|
45
|
+
end
|
46
|
+
|
47
|
+
task :default do
|
48
|
+
Rake::Task['rubocop'].execute
|
49
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Foreman
|
4
|
+
module Controller
|
5
|
+
module Parameters
|
6
|
+
module ResourceQuota
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def resource_quota_params_filter
|
11
|
+
Foreman::ParameterFilter.new(::ForemanResourceQuota::ResourceQuota).tap do |filter|
|
12
|
+
filter.permit :name
|
13
|
+
filter.permit :description
|
14
|
+
filter.permit :cpu_cores
|
15
|
+
filter.permit :memory_mb
|
16
|
+
filter.permit :disk_gb
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def resource_quota_params
|
22
|
+
param_name = parameter_filter_context.api? ? 'resource_quota' : 'foreman_resource_quota_resource_quota'
|
23
|
+
self.class.resource_quota_params_filter.filter_params(params, parameter_filter_context, param_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module Api
|
5
|
+
module V2
|
6
|
+
class ResourceQuotasController < ::Api::V2::BaseController
|
7
|
+
include ::Api::Version2
|
8
|
+
include Foreman::Controller::Parameters::ResourceQuota
|
9
|
+
|
10
|
+
resource_description do
|
11
|
+
resource_id 'resource_quota'
|
12
|
+
api_version 'v2'
|
13
|
+
api_base_url '/foreman_resource_quota/api'
|
14
|
+
end
|
15
|
+
|
16
|
+
before_action :find_resource, only: %i[show update destroy]
|
17
|
+
before_action :custom_find_resource, only: %i[utilization hosts users usergroups]
|
18
|
+
|
19
|
+
api :GET, '/resource_quotas', N_('List all resource quotas')
|
20
|
+
param_group :search_and_pagination, ::Api::V2::BaseController
|
21
|
+
add_scoped_search_description_for(ForemanResourceQuota::ResourceQuota)
|
22
|
+
def index
|
23
|
+
@resource_quotas = resource_scope_for_index
|
24
|
+
end
|
25
|
+
|
26
|
+
api :GET, '/resource_quotas/:id/', N_('Show resource quota')
|
27
|
+
param :id, :identifier, required: true
|
28
|
+
def show
|
29
|
+
end
|
30
|
+
|
31
|
+
api :GET, '/resource_quotas/:id/utilization', N_('Show used resources of assigned hosts')
|
32
|
+
param :id, :identifier, required: true
|
33
|
+
def utilization
|
34
|
+
@resource_quota.determine_utilization
|
35
|
+
process_response @resource_quota
|
36
|
+
end
|
37
|
+
|
38
|
+
api :GET, '/resource_quotas/:id/hosts', N_('Show hosts of a resource quota')
|
39
|
+
param :id, :identifier, required: true
|
40
|
+
def hosts
|
41
|
+
process_response @resource_quota.hosts
|
42
|
+
end
|
43
|
+
|
44
|
+
api :GET, '/resource_quotas/:id/users', N_('Show users of a resource quota')
|
45
|
+
param :id, :identifier, required: true
|
46
|
+
def users
|
47
|
+
process_response @resource_quota.users
|
48
|
+
end
|
49
|
+
|
50
|
+
api :GET, '/resource_quotas/:id/usergroups', N_('Show usergroups of a resource quota')
|
51
|
+
param :id, :identifier, required: true
|
52
|
+
def usergroups
|
53
|
+
process_response @resource_quota.usergroups
|
54
|
+
end
|
55
|
+
|
56
|
+
def_param_group :resource_quota do
|
57
|
+
param :resource_quota, Hash, required: true, action_aware: true do
|
58
|
+
param :name, String, required: true
|
59
|
+
# param :operatingsystem_ids, Array, :desc => N_("Operating system IDs")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
api :POST, '/resource_quotas/', N_('Create a resource quota')
|
64
|
+
param_group :resource_quota, as: :create
|
65
|
+
def create
|
66
|
+
@resource_quota = ForemanResourceQuota::ResourceQuota.new(resource_quota_params)
|
67
|
+
process_response @resource_quota.save
|
68
|
+
end
|
69
|
+
|
70
|
+
api :PUT, '/resource_quotas/:id/', N_('Update a resource quota')
|
71
|
+
param :id, :identifier, required: true
|
72
|
+
param_group :resource_quota
|
73
|
+
def update
|
74
|
+
process_response @resource_quota.update(resource_quota_params)
|
75
|
+
end
|
76
|
+
|
77
|
+
api :DELETE, '/resource_quotas/:id/', N_('Delete a resource quota')
|
78
|
+
param :id, :identifier, required: true
|
79
|
+
def destroy
|
80
|
+
process_response @resource_quota.destroy
|
81
|
+
end
|
82
|
+
|
83
|
+
def resource_class
|
84
|
+
ForemanResourceQuota::ResourceQuota
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def custom_find_resource
|
90
|
+
@resource_quota = ForemanResourceQuota::ResourceQuota.find_by(id: params[:resource_quota_id])
|
91
|
+
not_found unless @resource_quota
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
class ResourceQuotasController < ::ForemanResourceQuota::ApplicationController
|
5
|
+
include Foreman::Controller::AutoCompleteSearch
|
6
|
+
include Foreman::Controller::Parameters::ResourceQuota
|
7
|
+
|
8
|
+
before_action :find_resource, only: %i[edit update destroy]
|
9
|
+
|
10
|
+
def index
|
11
|
+
@resource_quotas = resource_base.search_for(params[:search], order: params[:order]).paginate(page: params[:page],
|
12
|
+
per_page: params[:per_page])
|
13
|
+
# TODO: Check necessitiy/purpose of authorizer
|
14
|
+
# AuthorizerHelper#authorizer uses controller_name as variable name, but it fails with namespaces
|
15
|
+
# @authorizer = Authorizer.new(User.current, collection: @resource_quotas)
|
16
|
+
end
|
17
|
+
|
18
|
+
def new
|
19
|
+
@resource_quota = ResourceQuota.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def create
|
23
|
+
@resource_quota = ResourceQuota.new(resource_quota_params)
|
24
|
+
if @resource_quota.save
|
25
|
+
process_success
|
26
|
+
else
|
27
|
+
process_error
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def edit
|
32
|
+
end
|
33
|
+
|
34
|
+
def update
|
35
|
+
if @resource_quota.update(resource_quota_params)
|
36
|
+
process_success
|
37
|
+
else
|
38
|
+
process_error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy
|
43
|
+
if @resource_quota.destroy
|
44
|
+
process_success
|
45
|
+
else
|
46
|
+
process_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module HostsHelper
|
5
|
+
def resource_quota_select(form, user_quotas)
|
6
|
+
blank_opt = { include_blank: true }
|
7
|
+
select_items = user_quotas.order(:name)
|
8
|
+
select_f form,
|
9
|
+
:resource_quota_id,
|
10
|
+
select_items,
|
11
|
+
:id,
|
12
|
+
:to_label,
|
13
|
+
blank_opt,
|
14
|
+
label: _('Resource Quota'),
|
15
|
+
help_inline: _('Define the Resource Quota this host counts to.')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module ResourceQuotaHelper
|
5
|
+
FACTOR_B_TO_KB = 1024
|
6
|
+
FACTOR_B_TO_MB = 1024 * 1024
|
7
|
+
FACTOR_B_TO_GB = 1024 * FACTOR_B_TO_MB
|
8
|
+
|
9
|
+
def natural_resource_name_by_type(resource_type)
|
10
|
+
type_names = { cpu_cores: 'CPU cores', memory_mb: 'Memory', disk_gb: 'Disk space' }
|
11
|
+
key = resource_type.is_a?(String) ? resource_type.to_sym : resource_type
|
12
|
+
raise "No natural name for unknown resource type '#{resource_type}'" unless type_names.key?(key)
|
13
|
+
type_names[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def units_by_type(resource_type)
|
17
|
+
type_units = {
|
18
|
+
cpu_cores: [
|
19
|
+
{ symbol: 'cores', factor: 1 },
|
20
|
+
],
|
21
|
+
memory_mb: [
|
22
|
+
{ symbol: 'MB', factor: 1 },
|
23
|
+
{ symbol: 'GB', factor: FACTOR_B_TO_KB },
|
24
|
+
{ symbol: 'TB', factor: FACTOR_B_TO_MB },
|
25
|
+
],
|
26
|
+
disk_gb: [
|
27
|
+
{ symbol: 'GB', factor: 1 },
|
28
|
+
{ symbol: 'TB', factor: FACTOR_B_TO_KB },
|
29
|
+
{ symbol: 'PB', factor: FACTOR_B_TO_MB },
|
30
|
+
],
|
31
|
+
}
|
32
|
+
key = resource_type.to_sym
|
33
|
+
raise "No units for unknown resource type '#{resource_type}'" unless type_units.key?(key)
|
34
|
+
type_units[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_largest_unit(resource_value, units)
|
38
|
+
units.reverse_each do |unit|
|
39
|
+
return unit.values_at(:symbol, :factor) if resource_value >= unit[:factor]
|
40
|
+
end
|
41
|
+
units[0].values_at(:symbol, :factor)
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_value_to_string(resource_value, resource_type)
|
45
|
+
(symbol, factor) = find_largest_unit(resource_value, units_by_type(resource_type))
|
46
|
+
unit_applied_value = (resource_value / factor).round(1)
|
47
|
+
format_text = if (unit_applied_value % 1).zero?
|
48
|
+
'%.0f %s'
|
49
|
+
else
|
50
|
+
'%.1f %s'
|
51
|
+
end
|
52
|
+
format(format_text, unit_applied_value, symbol)
|
53
|
+
end
|
54
|
+
|
55
|
+
def utilization_from_resource_origins(resources, hosts, custom_resource_origins: nil)
|
56
|
+
utilization_sum = resources.each.with_object({}) { |key, hash| hash[key] = 0 }
|
57
|
+
missing_hosts_resources = create_missing_hosts_resources_hash(hosts, resources)
|
58
|
+
hosts_hash = hosts.index_by(&:name)
|
59
|
+
resource_classes = custom_resource_origins || default_resource_origin_classes
|
60
|
+
resource_classes.each do |origin_class|
|
61
|
+
origin_class.new.collect_resources!(
|
62
|
+
utilization_sum,
|
63
|
+
missing_hosts_resources,
|
64
|
+
hosts_hash
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
[utilization_sum, missing_hosts_resources]
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Create a Hash that maps resources to host names.
|
74
|
+
# { <host name>: [<list of to be determined resources>] }
|
75
|
+
# for example:
|
76
|
+
# {
|
77
|
+
# "host_a": {
|
78
|
+
# [ :cpu_cores, :disk_gb ]
|
79
|
+
# },
|
80
|
+
# "host_b": {
|
81
|
+
# [ :cpu_cores, :disk_gb ]
|
82
|
+
# },
|
83
|
+
# Parameters:
|
84
|
+
# - hosts: Array of host objects.
|
85
|
+
# - resources: Array of resources (as symbol, e.g. [:cpu_cores, :disk_gb]).
|
86
|
+
# Returns: Hash with host names as keys and resources as values.
|
87
|
+
def create_missing_hosts_resources_hash(hosts, resources)
|
88
|
+
return {} if hosts.empty? || resources.empty?
|
89
|
+
|
90
|
+
resources_to_determine = resources.compact
|
91
|
+
return {} if resources_to_determine.empty?
|
92
|
+
|
93
|
+
hosts.map(&:name).index_with { resources_to_determine.clone }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Default classes that are used to determine host resources. Determines
|
97
|
+
# resources in the order of this list.
|
98
|
+
def default_resource_origin_classes
|
99
|
+
[
|
100
|
+
ResourceOrigins::ComputeResourceOrigin,
|
101
|
+
ResourceOrigins::VMAttributesOrigin,
|
102
|
+
ResourceOrigins::ComputeAttributesOrigin,
|
103
|
+
ResourceOrigins::FactsOrigin,
|
104
|
+
]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module HostManagedExtensions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ForemanResourceQuota::ResourceQuotaHelper
|
7
|
+
include ForemanResourceQuota::Exceptions
|
8
|
+
|
9
|
+
included do
|
10
|
+
validate :check_resource_quota_capacity
|
11
|
+
|
12
|
+
belongs_to :resource_quota, class_name: '::ForemanResourceQuota::ResourceQuota'
|
13
|
+
scoped_search relation: :resource_quota, on: :name, complete_value: true, rename: :resource_quota
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_resource_quota_capacity
|
17
|
+
handle_quota_check
|
18
|
+
true
|
19
|
+
rescue ResourceQuotaException => e
|
20
|
+
handle_error('resource_quota_id',
|
21
|
+
e.bare_message,
|
22
|
+
format('An error occured while checking the resource quota capacity: %s', e))
|
23
|
+
rescue Foreman::Exception => e
|
24
|
+
handle_error(:base,
|
25
|
+
e.bare_message,
|
26
|
+
format('An unexpected Foreman error occured while checking the resource quota capacity: %s', e))
|
27
|
+
rescue StandardError => e
|
28
|
+
handle_error(:base,
|
29
|
+
e.message,
|
30
|
+
format('An unknown error occured while checking the resource quota capacity: %s', e))
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def handle_quota_check
|
36
|
+
return if early_return?
|
37
|
+
quota_utilization = determine_quota_utilization
|
38
|
+
host_resources = determine_host_resources
|
39
|
+
verify_resource_quota_limits(quota_utilization, host_resources)
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_error(error_module, error_message, log_message)
|
43
|
+
errors.add(error_module, error_message)
|
44
|
+
Rails.logger.error(N_(log_message))
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def determine_quota_utilization
|
49
|
+
resource_quota.determine_utilization
|
50
|
+
missing_hosts = resource_quota.missing_hosts
|
51
|
+
unless missing_hosts.empty?
|
52
|
+
raise ResourceQuotaUtilizationException,
|
53
|
+
"Resource Quota '#{resource_quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
|
54
|
+
end
|
55
|
+
resource_quota.utilization
|
56
|
+
end
|
57
|
+
|
58
|
+
def determine_host_resources
|
59
|
+
(host_resources, missing_hosts) = call_utilization_helper(resource_quota.active_resources, [self])
|
60
|
+
unless missing_hosts.empty?
|
61
|
+
raise HostResourcesException,
|
62
|
+
"Cannot determine host resources for #{name}"
|
63
|
+
end
|
64
|
+
host_resources
|
65
|
+
end
|
66
|
+
|
67
|
+
def verify_resource_quota_limits(quota_utilization, host_resources)
|
68
|
+
quota_utilization.each do |resource_type, resource_utilization|
|
69
|
+
next if resource_utilization.nil?
|
70
|
+
|
71
|
+
max_quota = resource_quota[resource_type]
|
72
|
+
all_hosts_utilization = resource_utilization + host_resources[resource_type.to_sym]
|
73
|
+
next if all_hosts_utilization <= max_quota
|
74
|
+
|
75
|
+
raise ResourceLimitException, formulate_limit_error(resource_utilization,
|
76
|
+
all_hosts_utilization, max_quota, resource_type)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def formulate_limit_error(resource_utilization, all_hosts_utilization, max_quota, resource_type)
|
81
|
+
if resource_utilization < max_quota
|
82
|
+
N_(format("Host exceeds %s limit of '%s'-quota by %s (max. %s)",
|
83
|
+
natural_resource_name_by_type(resource_type),
|
84
|
+
resource_quota.name,
|
85
|
+
resource_value_to_string(all_hosts_utilization - max_quota, resource_type),
|
86
|
+
resource_value_to_string(max_quota, resource_type)))
|
87
|
+
else
|
88
|
+
N_(format("%s limit of '%s'-quota is already exceeded by %s without adding the new host (max. %s)",
|
89
|
+
natural_resource_name_by_type(resource_type),
|
90
|
+
resource_quota.name,
|
91
|
+
resource_value_to_string(resource_utilization - max_quota, resource_type),
|
92
|
+
resource_value_to_string(max_quota, resource_type)))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def early_return?
|
97
|
+
if resource_quota.nil?
|
98
|
+
return true if quota_assigment_optional?
|
99
|
+
raise HostResourceQuotaEmptyException, 'must be given.'
|
100
|
+
end
|
101
|
+
return true if resource_quota.active_resources.empty?
|
102
|
+
return true if Setting[:resource_quota_global_no_action] # quota is assigned, but not supposed to be checked
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
def quota_assigment_optional?
|
107
|
+
owner.resource_quota_is_optional || Setting[:resource_quota_optional_assignment]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Wrap into a function for easier testing
|
111
|
+
def call_utilization_helper(resources, hosts)
|
112
|
+
utilization_from_resource_origins(resources, hosts)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module UserExtensions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
has_many :resource_quota_users, class_name: 'ForemanResourceQuota::ResourceQuotaUser', dependent: :destroy,
|
8
|
+
inverse_of: :user
|
9
|
+
has_many :resource_quotas, class_name: 'ForemanResourceQuota::ResourceQuota', through: :resource_quota_users
|
10
|
+
attribute :resource_quota_is_optional, :boolean, default: false
|
11
|
+
|
12
|
+
scoped_search relation: :resource_quotas, on: :name, complete_value: true, rename: :resource_quota
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
module UsergroupExtensions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
has_many :resource_quota_usergroups, class_name: 'ForemanResourceQuota::ResourceQuotaUsergroup',
|
8
|
+
dependent: :destroy, inverse_of: :usergroup
|
9
|
+
has_many :resource_quotas, class_name: 'ForemanResourceQuota::ResourceQuota', through: :resource_quota_usergroups
|
10
|
+
|
11
|
+
scoped_search relation: :resource_quotas, on: :name, complete_value: true, rename: :resource_quota
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
class ResourceQuota < ApplicationRecord
|
5
|
+
include ResourceQuotaHelper
|
6
|
+
include Authorizable
|
7
|
+
include Parameterizable::ByIdName
|
8
|
+
extend FriendlyId
|
9
|
+
friendly_id :name
|
10
|
+
audited
|
11
|
+
|
12
|
+
self.table_name = 'resource_quotas'
|
13
|
+
|
14
|
+
has_many :resource_quota_users, class_name: 'ResourceQuotaUser', inverse_of: :resource_quota, dependent: :destroy
|
15
|
+
has_many :users, class_name: '::User', through: :resource_quota_users
|
16
|
+
has_many :resource_quota_usergroups, class_name: 'ResourceQuotaUsergroup', inverse_of: :resource_quota,
|
17
|
+
dependent: :destroy
|
18
|
+
has_many :usergroups, class_name: '::Usergroup', through: :resource_quota_usergroups
|
19
|
+
has_many :hosts, class_name: '::Host::Managed', dependent: :nullify
|
20
|
+
|
21
|
+
validates :name, presence: true, uniqueness: true
|
22
|
+
|
23
|
+
scoped_search on: :name, complete_value: true
|
24
|
+
scoped_search on: :id, complete_enabled: false, only_explicit: true, validator: ScopedSearch::Validators::INTEGER
|
25
|
+
|
26
|
+
attribute :utilization, :jsonb, default: {}
|
27
|
+
attribute :missing_hosts, :jsonb, default: {}
|
28
|
+
|
29
|
+
def number_of_hosts
|
30
|
+
hosts.size
|
31
|
+
end
|
32
|
+
|
33
|
+
def number_of_users
|
34
|
+
users.size
|
35
|
+
end
|
36
|
+
|
37
|
+
def number_of_usergroups
|
38
|
+
usergroups.size
|
39
|
+
end
|
40
|
+
|
41
|
+
def determine_utilization(additional_hosts = [])
|
42
|
+
quota_hosts = (hosts | (additional_hosts))
|
43
|
+
self.utilization, self.missing_hosts = call_utilization_helper(quota_hosts)
|
44
|
+
|
45
|
+
print_warning(missing_hosts, quota_hosts) unless missing_hosts.empty?
|
46
|
+
rescue StandardError => e
|
47
|
+
print_error(e) # print error log here and forward error
|
48
|
+
raise e
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_label
|
52
|
+
name
|
53
|
+
end
|
54
|
+
|
55
|
+
def active_resources
|
56
|
+
resources = []
|
57
|
+
%i[cpu_cores memory_mb disk_gb].each do |res|
|
58
|
+
resources << res unless self[res].nil?
|
59
|
+
end
|
60
|
+
resources
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Wrap into a function for easier testing
|
66
|
+
def call_utilization_helper(quota_hosts)
|
67
|
+
utilization_from_resource_origins(active_resources, quota_hosts)
|
68
|
+
end
|
69
|
+
|
70
|
+
def print_warning(missing_hosts, hosts)
|
71
|
+
warn_text = "Could not determines resources for #{missing_hosts.size} hosts:"
|
72
|
+
missing_hosts.each do |host_id, missing_resources|
|
73
|
+
missing_host = hosts.find { |obj| obj.id == host_id }
|
74
|
+
warn_text << " '#{missing_host.name}': '#{missing_resources}'\n" unless missing_host.nil?
|
75
|
+
end
|
76
|
+
Rails.logger.warn warn_text
|
77
|
+
end
|
78
|
+
|
79
|
+
def print_error(err)
|
80
|
+
Rails.logger.error("An error occured while determining resources for quota '#{name}': #{err}")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
class ResourceQuotaUser < ApplicationRecord
|
5
|
+
self.table_name = 'resource_quotas_users'
|
6
|
+
|
7
|
+
belongs_to :resource_quota, inverse_of: :resource_quota_users
|
8
|
+
belongs_to :user, class_name: '::User', inverse_of: :resource_quota_users
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanResourceQuota
|
4
|
+
class ResourceQuotaUsergroup < ApplicationRecord
|
5
|
+
self.table_name = 'resource_quotas_usergroups'
|
6
|
+
|
7
|
+
belongs_to :resource_quota, inverse_of: :resource_quota_usergroups
|
8
|
+
belongs_to :usergroup, class_name: '::Usergroup', inverse_of: :resource_quota_usergroups
|
9
|
+
end
|
10
|
+
end
|