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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanResourceQuota
4
+ class ResourceOrigin
5
+ RESOURCE_FUNCTION_MAP = {
6
+ cpu_cores: :extract_cpu_cores,
7
+ memory_mb: :extract_memory_mb,
8
+ disk_gb: :extract_disk_gb,
9
+ }.freeze
10
+
11
+ def collect_resources!(resources_sum, missing_hosts_resources, host_objects)
12
+ return if missing_hosts_resources.empty?
13
+
14
+ relevant_hosts = load_hosts_eagerly(missing_hosts_resources, host_objects, host_attribute_eager_name)
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)
17
+ end
18
+
19
+ def host_attribute_eager_name
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def host_attribute_name
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def extract_cpu_cores(param)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def extract_memory_mb(param)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def extract_disk_gb(param)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ private
40
+
41
+ def load_hosts_eagerly(missing_hosts_resources, host_objects, eager_attribute)
42
+ relevant_hosts = Host::Managed.where(name: missing_hosts_resources.keys).includes(eager_attribute)
43
+ relevant_hosts = relevant_hosts.compact
44
+ if relevant_hosts.size < missing_hosts_resources.size # Add non-eagerly loaded host objects
45
+ relevant_hosts_names = relevant_hosts.map(&:name)
46
+ (missing_hosts_resources.keys - relevant_hosts_names).each do |missing_host_name|
47
+ relevant_hosts << host_objects[missing_host_name]
48
+ end
49
+ end
50
+ relevant_hosts
51
+ rescue ActiveRecord::AssociationNotFoundError
52
+ # the eager_attribute could not be loaded
53
+ host_objects.values
54
+ end
55
+
56
+ def collect_attribute_from_hosts(host_list, attribute_name)
57
+ host_values = {}
58
+ host_list.each do |host|
59
+ attribute_value = host.send(attribute_name)
60
+ host_values[host.name] = attribute_value if attribute_value.present?
61
+ rescue StandardError
62
+ # skip hosts whose attribute couldn't be collected. They will be kept in the list
63
+ # of missing host resources
64
+ end
65
+ host_values
66
+ end
67
+
68
+ def sum_resources_and_delete_missing_hosts!(resources_sum, missing_hosts_resources, host_values)
69
+ host_values.each do |host_name, attribute_content|
70
+ missing_hosts_resources[host_name].reverse_each do |resource_name|
71
+ resource_value = process_resource(resource_name, attribute_content)
72
+ next unless resource_value
73
+ resources_sum[resource_name] += resource_value
74
+ missing_hosts_resources[host_name].delete(resource_name)
75
+ end
76
+ missing_hosts_resources.delete(host_name) if missing_hosts_resources[host_name].empty?
77
+ end
78
+ end
79
+
80
+ def process_resource(resource_name, attribute_content)
81
+ resource_value = method(RESOURCE_FUNCTION_MAP[resource_name]).call(attribute_content)
82
+ return nil unless resource_value
83
+ resource_value
84
+ end
85
+
86
+ # Extract encapsulated volumes attributes
87
+ # The structure of data encapslated in :volumes_attributes is inconsistent. For example,
88
+ # in an API call for host creation call this is an Array. But, through the UI this becomes a Hash.
89
+ # Parameters: A Hash containing :volumes_attributes.
90
+ # Returns: An Array of volume data (expecting an Array of Hashes)
91
+ def extract_volumes(param)
92
+ volumes = param[:volumes_attributes]
93
+ volumes = volumes.values if volumes.is_a?(Hash) # If it's a Hash, extract values
94
+ volumes
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanResourceQuota
4
+ module ResourceOrigins
5
+ class ComputeAttributesOrigin < ResourceOrigin
6
+ def host_attribute_eager_name
7
+ :compute_attributes
8
+ end
9
+
10
+ def host_attribute_name
11
+ :compute_attributes
12
+ end
13
+
14
+ def extract_cpu_cores(param)
15
+ return nil unless param.key?(:cpus)
16
+ param[:cpus].to_i
17
+ rescue StandardError
18
+ nil
19
+ end
20
+
21
+ def extract_memory_mb(param)
22
+ case determine_memory_key(param)
23
+ when :memory
24
+ param[:memory].to_i / ResourceQuotaHelper::FACTOR_B_TO_MB
25
+ when :memory_mb
26
+ param[:memory_mb].to_i
27
+ end
28
+ rescue StandardError
29
+ nil
30
+ end
31
+
32
+ def extract_disk_gb(param)
33
+ return nil unless param.key?(:volumes_attributes)
34
+ extract_volumes(param).sum do |disk|
35
+ # key can be capactiy or size_gb
36
+ return nil unless disk.key?(:capacity) || disk.key?(:size_gb)
37
+ (disk[:size_gb] || disk[:capacity]).to_i
38
+ end
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
43
+ private
44
+
45
+ def determine_memory_key(param)
46
+ return :memory if param.key?(:memory)
47
+ return :memory_mb if param.key?(:memory_mb)
48
+ nil
49
+ end
50
+
51
+ def parse_storage_string(storage_str)
52
+ return nil unless storage_str.is_a? String
53
+ case storage_str[-1].upcase
54
+ when 'G'
55
+ storage_str.to_i
56
+ when 'T'
57
+ storage_str[0..-2].to_i * ResourceQuotaHelper::FACTOR_B_TO_MB
58
+ when 'M'
59
+ (storage_str[0..-2].to_f / ResourceQuotaHelper::FACTOR_B_TO_MB).ceil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanResourceQuota
4
+ module ResourceOrigins
5
+ class ComputeResourceOrigin < ResourceOrigin
6
+ def extract_cpu_cores(param)
7
+ param.cpus
8
+ rescue StandardError
9
+ nil
10
+ end
11
+
12
+ def extract_memory_mb(param)
13
+ param.memory.to_i / ResourceQuotaHelper::FACTOR_B_TO_MB
14
+ rescue StandardError
15
+ nil
16
+ end
17
+
18
+ def extract_disk_gb(_)
19
+ # FIXME: no disk given in VM (compare fog extensions)
20
+ nil
21
+ end
22
+
23
+ def collect_resources!(resources_sum, missing_hosts_resources, _host_objects)
24
+ compute_resource_to_hosts = group_hosts_by_compute_resource(missing_hosts_resources.keys)
25
+
26
+ compute_resource_to_hosts.each do |compute_resource_id, hosts|
27
+ next if compute_resource_id == :nil_compute_resource
28
+
29
+ host_vms, vm_id_attr = filter_vms_by_hosts(hosts, compute_resource_id)
30
+ next if host_vms.empty?
31
+
32
+ hosts.each do |host|
33
+ vm = host_vms.find { |obj| obj.send(vm_id_attr) == host.uuid }
34
+ next unless vm
35
+ process_host_vm!(resources_sum, missing_hosts_resources, host.name, vm)
36
+ end
37
+ end
38
+ end
39
+
40
+ # Groups hosts with the same compute resource.
41
+ # Parameters: An array of host names.
42
+ # Returns: A hash where keys represent compute resource IDs and values are the list of host objects.
43
+ def group_hosts_by_compute_resource(host_names)
44
+ Host::Managed.where(name: host_names).includes(:compute_resource).group_by do |host|
45
+ host.compute_resource&.id || :nil_compute_resource
46
+ end
47
+ end
48
+
49
+ # Filters VMs of a compute resource, given a list of hosts.
50
+ # Parameters:
51
+ # - hosts: An array of host objects.
52
+ # - compute_resource_id: ID of the compute resource.
53
+ # Returns:
54
+ # - filtered_vms: An array of filtered virtual machine objects.
55
+ # - id_attr: Attribute used for identifying virtual machines (either :vmid or :id).
56
+ def filter_vms_by_hosts(hosts, compute_resource_id)
57
+ host_uuids = hosts.map(&:uuid)
58
+ vms = ComputeResource.find_by(id: compute_resource_id).vms.all
59
+ id_attr = vms[0].respond_to?(:vmid) ? :vmid : :id
60
+ filtered_vms = vms.select { |obj| host_uuids.include?(obj.send(id_attr)) } # reduce from all vms
61
+ [filtered_vms, id_attr]
62
+ end
63
+
64
+ # Processes a host's virtual machines and updates resource allocation.
65
+ # Parameters:
66
+ # - resources_sum: Hash containing total resources sum.
67
+ # - missing_hosts_resources: Hash containing missing resources per host.
68
+ # - host_name: Name of the host.
69
+ # - vm: Compute resource VM object of given host.
70
+ # Returns: None.
71
+ def process_host_vm!(resources_sum, missing_hosts_resources, host_name, host_vm)
72
+ missing_hosts_resources[host_name].reverse_each do |resource_name|
73
+ resource_value = process_resource(resource_name, host_vm)
74
+ next unless resource_value
75
+ resources_sum[resource_name] += resource_value
76
+ missing_hosts_resources[host_name].delete(resource_name)
77
+ end
78
+ missing_hosts_resources.delete(host_name) if missing_hosts_resources[host_name].empty?
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanResourceQuota
4
+ module ResourceOrigins
5
+ class FactsOrigin < ResourceOrigin
6
+ FACTS_KEYS_CPU_CORES = [
7
+ 'ansible_processor_cores',
8
+ 'facter_processors::cores',
9
+ 'proc_cpuinfo::common::cpu_cores',
10
+ 'processors::cores',
11
+ ].freeze
12
+
13
+ FACTS_KEYS_MEMORY_B = [
14
+ 'facter_memory::system::total_bytes',
15
+ 'memory::system::total_bytes',
16
+ 'memory::memtotal',
17
+ ].freeze
18
+
19
+ FACTS_REGEX_DISK_B = [
20
+ /^disks::(\w+)::size_bytes$/,
21
+ /^facter_disks::(\w+)::size_bytes$/,
22
+ ].freeze
23
+
24
+ def host_attribute_eager_name
25
+ :fact_values
26
+ end
27
+
28
+ def host_attribute_name
29
+ :facts
30
+ end
31
+
32
+ def extract_cpu_cores(param)
33
+ common_keys = param.keys & FACTS_KEYS_CPU_CORES
34
+ return param[common_keys.first].to_i if common_keys.any?
35
+ nil
36
+ rescue StandardError
37
+ nil
38
+ end
39
+
40
+ def extract_memory_mb(param)
41
+ common_keys = param.keys & FACTS_KEYS_MEMORY_B
42
+ return (param[common_keys.first].to_i / ResourceQuotaHelper::FACTOR_B_TO_MB).to_i if common_keys.any?
43
+ nil
44
+ rescue StandardError
45
+ nil
46
+ end
47
+
48
+ def extract_disk_gb(param)
49
+ total_gb = nil
50
+
51
+ FACTS_REGEX_DISK_B.each do |regex|
52
+ relevant_keys = param.keys.grep(regex)
53
+ next unless relevant_keys.any?
54
+
55
+ total_gb = sum_disk_space(param, relevant_keys)
56
+ end
57
+
58
+ total_gb&.to_i
59
+ rescue StandardError
60
+ nil
61
+ end
62
+
63
+ def sum_disk_space(facts, keys)
64
+ keys.map { |key| facts[key].to_i }.sum / ResourceQuotaHelper::FACTOR_B_TO_GB unless keys.empty?
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanResourceQuota
4
+ module ResourceOrigins
5
+ class VMAttributesOrigin < ResourceOrigin
6
+ def host_attribute_eager_name
7
+ :compute_resource
8
+ end
9
+
10
+ def host_attribute_name
11
+ :vm_compute_attributes
12
+ end
13
+
14
+ def extract_cpu_cores(param)
15
+ return nil unless param.key?(:cpus)
16
+ param[:cpus]
17
+ rescue StandardError
18
+ nil
19
+ end
20
+
21
+ def extract_memory_mb(param)
22
+ return nil unless param.key?(:memory_mb)
23
+ param[:memory_mb]
24
+ rescue StandardError
25
+ nil
26
+ end
27
+
28
+ def extract_disk_gb(param)
29
+ return nil unless param.key?(:volumes_attributes)
30
+ extract_volumes(param).sum do |disk|
31
+ # key can be capactiy or size_gb
32
+ return nil unless disk.key?(:capacity) || disk.key?(:size_gb)
33
+ (disk[:size_gb] || disk[:capacity]).to_i
34
+ end
35
+ rescue StandardError
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ attribute :resource_quota_id, :resource_quota_name
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ attributes :name, :id, :description, :cpu_cores, :memory_mb, :disk_gb, :number_of_hosts, :number_of_users,
6
+ :number_of_usergroups
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ extends 'api/v2/resource_quotas/show'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ child :hosts do
6
+ extends 'api/v2/hosts/base'
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ collection @resource_quotas
4
+
5
+ extends 'api/v2/resource_quotas/main'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ extends 'api/v2/resource_quotas/base'
6
+
7
+ attributes :created_at, :updated_at
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ extends 'api/v2/resource_quotas/main'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ extends 'api/v2/resource_quotas/show'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ child :usergroups do
6
+ extends 'api/v2/usergroups/base'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ child :users do
6
+ extends 'api/v2/users/base'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @resource_quota
4
+
5
+ extends 'api/v2/resource_quotas/main'
6
+
7
+ attributes :utilization
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ attribute :resource_quota_ids, :resource_quota_names
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ attribute :resource_quota_ids, :resource_quota_names
@@ -0,0 +1,21 @@
1
+ <%
2
+ if @resource_quota.new_record?
3
+ react_data = {
4
+ "isNewQuota": true,
5
+ }
6
+ else
7
+ react_data = {
8
+ "isNewQuota": false,
9
+ "initialProperties": {
10
+ "id": @resource_quota.id,
11
+ "name": @resource_quota.name,
12
+ "description": @resource_quota.description,
13
+ "cpu_cores": @resource_quota.cpu_cores,
14
+ "memory_mb": @resource_quota.memory_mb,
15
+ "disk_gb": @resource_quota.disk_gb,
16
+ },
17
+ }
18
+ end
19
+ %>
20
+
21
+ <%= react_component('ResourceQuotaForm', react_data) %>
@@ -0,0 +1,12 @@
1
+ <% title(_('Edit Resource Quota %s') % @resource_quota) %>
2
+ <%= breadcrumbs(resource_url: api_resource_quotas_path,
3
+ switcher_item_url: edit_resource_quota_path(':id')) %>
4
+
5
+ <% content_for(:javascripts) do %>
6
+ <%= webpacked_plugins_js_for :foreman_resource_quota %>
7
+ <% end %>
8
+ <% content_for(:stylesheets) do %>
9
+ <%= webpacked_plugins_css_for :foreman_resource_quota %>
10
+ <% end %>
11
+
12
+ <%= render :partial => 'form' %>
@@ -0,0 +1,55 @@
1
+ <% content_for(:javascripts) do %>
2
+ <%= webpacked_plugins_js_for :foreman_resource_quota %>
3
+ <% end %>
4
+ <% content_for(:stylesheets) do %>
5
+ <%= webpacked_plugins_css_for :foreman_resource_quota %>
6
+ <% end %>
7
+
8
+ <% title _('Resource quotas') %>
9
+
10
+ <%= title_actions react_component('CreateResourceQuotaModal') %>
11
+
12
+ <table class="<%= table_css_classes 'table-bordered table-striped' %>">
13
+ <thead>
14
+ <tr>
15
+ <th><%= sort :name, :as => s_('Resource Quota|Name') %></th>
16
+ <th class="col-md-6"><%= _('Description') %></th>
17
+ <th class="col-md-1"><%= _('CPU cores') %></th>
18
+ <th class="col-md-1"><%= _('Memory (MB)') %></th>
19
+ <th class="col-md-1"><%= _('Disk space (GB)') %></th>
20
+ <th><%= _('Actions') %></th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ <% @resource_quotas.each do |quota|
25
+ react_data = {
26
+ "isNewQuota": false,
27
+ "initialProperties": {
28
+ "id": quota.id,
29
+ "name": quota.name,
30
+ "description": quota.description,
31
+ "cpu_cores": quota.cpu_cores,
32
+ "memory_mb": quota.memory_mb,
33
+ "disk_gb": quota.disk_gb,
34
+ },
35
+ }
36
+ %>
37
+ <tr>
38
+ <td class="ellipsis">
39
+ <%= react_component('UpdateResourceQuotaModal', react_data) %>
40
+ </td>
41
+ <td><%= h(quota.description) %></td>
42
+ <td><%= h(quota.cpu_cores) %></td>
43
+ <td><%= h(quota.memory_mb) %></td>
44
+ <td><%= h(quota.disk_gb) %></td>
45
+ <td>
46
+ <%= action_buttons(
47
+ display_delete_if_authorized(hash_for_foreman_resource_quota_resource_quota_path(id: quota), data: { confirm: _("Delete %s?") % quota.name})
48
+ ) %>
49
+ </td>
50
+ </tr>
51
+ <% end %>
52
+ </tbody>
53
+ </table>
54
+
55
+ <%= will_paginate_with_info @resource_quotas %>
@@ -0,0 +1,10 @@
1
+ <% title _('New Resource Quota') %>
2
+
3
+ <% content_for(:javascripts) do %>
4
+ <%= webpacked_plugins_js_for :foreman_resource_quota %>
5
+ <% end %>
6
+ <% content_for(:stylesheets) do %>
7
+ <%= webpacked_plugins_css_for :foreman_resource_quota %>
8
+ <% end %>
9
+
10
+ <%= render :partial => 'form' %>
@@ -0,0 +1,10 @@
1
+ <% content_for(:javascripts) do %>
2
+ <%= webpacked_plugins_js_for :foreman_resource_quota %>
3
+ <% end %>
4
+ <% content_for(:stylesheets) do %>
5
+ <%= webpacked_plugins_css_for :foreman_resource_quota %>
6
+ <% end %>
7
+
8
+ <% content_for(:title, _("Resource Quotas")) %>
9
+
10
+ <%= react_component('ResourceQuotaEmptyState') %>
@@ -0,0 +1,4 @@
1
+ <%
2
+ user_quotas = User.current&.admin? ? ForemanResourceQuota::ResourceQuota.all : User.current.resource_quotas
3
+ %>
4
+ <%= resource_quota_select form, user_quotas %>
@@ -0,0 +1,45 @@
1
+ <% resource_type ||= pagelet.opts[:resource_type] %>
2
+ <% if resource_type == :user %>
3
+ <% resource = @user %>
4
+ <% elsif resource_type == :usergroup %>
5
+ <% resource = @usergroup %>
6
+ <% end %>
7
+
8
+ <% if resource_type == :user %>
9
+ <%= checkbox_f form, :"resource_quota_is_optional", :label => _("Optional Assignment"),
10
+ :label_help => _("It is optional for a user to assign a quota when creating new hosts") %>
11
+ <% end %>
12
+
13
+ <%= multiple_checkboxes(form, :resource_quotas, resource, ForemanResourceQuota::ResourceQuota, :label => _("Resource Quotas")) %>
14
+
15
+ <% if resource_type == :user %>
16
+ <% usergroups = @user.cached_usergroups.includes(:resource_quotas).distinct %>
17
+ <% if usergroups.any? %>
18
+ <div class="form-group" id="inherited-quotas">
19
+ <label class="col-md-2 control-label" for="quotas"><%= _("Quotas from user groups") %></label>
20
+ <div class="col-md-5">
21
+ <div class="dropdown">
22
+ <button class="btn btn-default dropdown-toggle" type="button" id="usergroupsDropdownMenuBtn" data-toggle="dropdown">
23
+ <%= usergroups.first %>
24
+ <span class="caret"></span>
25
+ </button>
26
+ <ul class="dropdown-menu" quota="menu" aria-labelledby="usergroupsDropdownMenuBtn">
27
+ <% usergroups.each do |usergroup| %>
28
+ <li quota="presentation"><a quota="menuitem" tabindex="-1" data-id="<%=usergroup.id %>"><%= usergroup %></a></li>
29
+ <% end %>
30
+ </ul>
31
+ </div>
32
+ <ul class="list-group" id='quotas_tab'>
33
+ <% usergroups.each do |usergroup| %>
34
+ <% unless usergroup.resource_quotas.map(&:name).any? %>
35
+ <li data-id="<%= usergroup.id %>" class="list-group-item"><%= _('This group has no quotas') %></li>
36
+ <%end %>
37
+ <% usergroup.resource_quotas.map(&:name).each do |quota_name| %>
38
+ <li data-id="<%= usergroup.id %>" class="list-group-item"><%= quota_name %></li>
39
+ <% end %>
40
+ <% end %>
41
+ </ul>
42
+ </div>
43
+ </div>
44
+ <% end %>
45
+ <% end %>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveSupport::Inflector.inflections do |inflect|
4
+ inflect.irregular 'resource_quota', 'resource_quotas'
5
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop: disable Metrics/BlockLength
4
+ Rails.application.routes.draw do
5
+ scope :foreman_resource_quota, path: 'foreman_resource_quota' do
6
+ resources :resource_quotas, except: %i[show], controller: 'foreman_resource_quota/resource_quotas' do
7
+ collection do
8
+ get 'auto_complete_search'
9
+ end
10
+ end
11
+ end
12
+
13
+ namespace :foreman_resource_quota do
14
+ resources :resource_quotas, except: %i[show] do
15
+ collection do
16
+ get 'help', action: :welcome
17
+ get 'auto_complete_search'
18
+ end
19
+ end
20
+
21
+ # API routes
22
+ namespace :api, defaults: { format: 'json' } do
23
+ scope '(:apiv)',
24
+ module: :v2,
25
+ defaults: { apiv: 'v2' },
26
+ apiv: /v1|v2/,
27
+ constraints: ApiConstraints.new(version: 2, default: true) do
28
+ resources :resource_quotas, except: %i[new edit] do
29
+ collection do
30
+ get 'auto_complete_search'
31
+ end
32
+ constraints(id: %r{[^/]+}) do
33
+ get 'utilization'
34
+ get 'hosts'
35
+ get 'users'
36
+ get 'usergroups'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ # rubocop: enable Metrics/BlockLength