foreman_resource_quota 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +619 -0
  3. data/README.md +51 -0
  4. data/Rakefile +49 -0
  5. data/app/controllers/concerns/foreman/controller/parameters/resource_quota.rb +28 -0
  6. data/app/controllers/foreman_resource_quota/api/v2/resource_quotas_controller.rb +96 -0
  7. data/app/controllers/foreman_resource_quota/application_controller.rb +9 -0
  8. data/app/controllers/foreman_resource_quota/resource_quotas_controller.rb +50 -0
  9. data/app/helpers/foreman_resource_quota/hosts_helper.rb +18 -0
  10. data/app/helpers/foreman_resource_quota/resource_quota_helper.rb +107 -0
  11. data/app/models/concerns/foreman_resource_quota/host_managed_extensions.rb +115 -0
  12. data/app/models/concerns/foreman_resource_quota/user_extensions.rb +15 -0
  13. data/app/models/concerns/foreman_resource_quota/usergroup_extensions.rb +14 -0
  14. data/app/models/foreman_resource_quota/resource_quota.rb +83 -0
  15. data/app/models/foreman_resource_quota/resource_quota_user.rb +10 -0
  16. data/app/models/foreman_resource_quota/resource_quota_usergroup.rb +10 -0
  17. data/app/services/foreman_resource_quota/resource_origin.rb +97 -0
  18. data/app/services/foreman_resource_quota/resource_origins/compute_attributes_origin.rb +64 -0
  19. data/app/services/foreman_resource_quota/resource_origins/compute_resource_origin.rb +82 -0
  20. data/app/services/foreman_resource_quota/resource_origins/facts_origin.rb +68 -0
  21. data/app/services/foreman_resource_quota/resource_origins/vm_attributes_origin.rb +40 -0
  22. data/app/views/foreman_resource_quota/api/v2/hosts/resource_quota.json.rabl +3 -0
  23. data/app/views/foreman_resource_quota/api/v2/resource_quotas/base.json.rabl +6 -0
  24. data/app/views/foreman_resource_quota/api/v2/resource_quotas/create.json.rabl +5 -0
  25. data/app/views/foreman_resource_quota/api/v2/resource_quotas/hosts.json.rabl +7 -0
  26. data/app/views/foreman_resource_quota/api/v2/resource_quotas/index.json.rabl +5 -0
  27. data/app/views/foreman_resource_quota/api/v2/resource_quotas/main.json.rabl +7 -0
  28. data/app/views/foreman_resource_quota/api/v2/resource_quotas/show.json.rabl +5 -0
  29. data/app/views/foreman_resource_quota/api/v2/resource_quotas/update.json.rabl +5 -0
  30. data/app/views/foreman_resource_quota/api/v2/resource_quotas/usergroups.json.rabl +7 -0
  31. data/app/views/foreman_resource_quota/api/v2/resource_quotas/users.json.rabl +7 -0
  32. data/app/views/foreman_resource_quota/api/v2/resource_quotas/utilization.json.rabl +7 -0
  33. data/app/views/foreman_resource_quota/api/v2/usergroups/resource_quota.json.rabl +3 -0
  34. data/app/views/foreman_resource_quota/api/v2/users/resource_quota.json.rabl +3 -0
  35. data/app/views/foreman_resource_quota/resource_quotas/_form.html.erb +21 -0
  36. data/app/views/foreman_resource_quota/resource_quotas/edit.html.erb +12 -0
  37. data/app/views/foreman_resource_quota/resource_quotas/index.html.erb +55 -0
  38. data/app/views/foreman_resource_quota/resource_quotas/new.html.erb +10 -0
  39. data/app/views/foreman_resource_quota/resource_quotas/welcome.html.erb +10 -0
  40. data/app/views/hosts/_form_quota_fields.html.erb +4 -0
  41. data/app/views/users/_form_quota_tab.html.erb +45 -0
  42. data/config/initializers/inflections.rb +5 -0
  43. data/config/routes.rb +43 -0
  44. data/db/migrate/20230306120001_create_resource_quotas.rb +31 -0
  45. data/lib/foreman_resource_quota/engine.rb +56 -0
  46. data/lib/foreman_resource_quota/exceptions.rb +11 -0
  47. data/lib/foreman_resource_quota/register.rb +106 -0
  48. data/lib/foreman_resource_quota/version.rb +5 -0
  49. data/lib/foreman_resource_quota.rb +6 -0
  50. data/lib/tasks/foreman_resource_quota_tasks.rake +50 -0
  51. data/locale/Makefile +60 -0
  52. data/locale/en/foreman_resource_quota.po +18 -0
  53. data/locale/foreman_resource_quota.pot +19 -0
  54. data/locale/gemspec.rb +4 -0
  55. data/package.json +44 -0
  56. data/webpack/api_helper.js +113 -0
  57. data/webpack/api_helper.test.js +96 -0
  58. data/webpack/components/CreateResourceQuotaModal.js +46 -0
  59. data/webpack/components/ResourceQuotaEmptyState/index.js +58 -0
  60. data/webpack/components/ResourceQuotaForm/ResourceQuotaForm.scss +1 -0
  61. data/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +71 -0
  62. data/webpack/components/ResourceQuotaForm/components/Properties/Properties.scss +9 -0
  63. data/webpack/components/ResourceQuotaForm/components/Properties/StaticDetail.js +72 -0
  64. data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.js +71 -0
  65. data/webpack/components/ResourceQuotaForm/components/Properties/StatusPropertiesLabel.test.js +50 -0
  66. data/webpack/components/ResourceQuotaForm/components/Properties/TextInputField.js +131 -0
  67. data/webpack/components/ResourceQuotaForm/components/Properties/index.js +190 -0
  68. data/webpack/components/ResourceQuotaForm/components/QuotaState.js +157 -0
  69. data/webpack/components/ResourceQuotaForm/components/Resource/Resource.scss +13 -0
  70. data/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +224 -0
  71. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.js +151 -0
  72. data/webpack/components/ResourceQuotaForm/components/Resource/UtilizationProgress.scss +10 -0
  73. data/webpack/components/ResourceQuotaForm/components/Resource/index.js +239 -0
  74. data/webpack/components/ResourceQuotaForm/components/Resources.js +105 -0
  75. data/webpack/components/ResourceQuotaForm/components/Submit.js +72 -0
  76. data/webpack/components/ResourceQuotaForm/index.js +185 -0
  77. data/webpack/components/UpdateResourceQuotaModal.js +143 -0
  78. data/webpack/global_index.js +15 -0
  79. data/webpack/global_test_setup.js +11 -0
  80. data/webpack/helper.js +86 -0
  81. data/webpack/index.js +23 -0
  82. data/webpack/lib/ActionableDetail.js +115 -0
  83. data/webpack/lib/ActionableDetail.scss +4 -0
  84. data/webpack/lib/EditableSwitch.js +47 -0
  85. data/webpack/lib/EditableTextInput/EditableTextInput.js +206 -0
  86. data/webpack/lib/EditableTextInput/PencilEditButton.js +27 -0
  87. data/webpack/lib/EditableTextInput/__tests__/editableTextInput.test.js +193 -0
  88. data/webpack/lib/EditableTextInput/editableTextInput.scss +38 -0
  89. data/webpack/lib/EditableTextInput/index.js +4 -0
  90. data/webpack/lib/react-testing-lib-wrapper.js +80 -0
  91. data/webpack/test_setup.js +17 -0
  92. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanResourceQuota
4
+ class ApplicationController < ::ApplicationController
5
+ def resource_class
6
+ "ForemanResourceQuota::#{controller_name.singularize.classify}".constantize
7
+ end
8
+ end
9
+ 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