foreman_statistics 0.1.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +619 -0
  3. data/README.md +40 -0
  4. data/Rakefile +47 -0
  5. data/app/controllers/concerns/foreman_statistics/parameters/trend.rb +20 -0
  6. data/app/controllers/foreman_statistics/api/v2/statistics_controller.rb +33 -0
  7. data/app/controllers/foreman_statistics/api/v2/trends_controller.rb +58 -0
  8. data/app/controllers/foreman_statistics/react_controller.rb +19 -0
  9. data/app/controllers/foreman_statistics/statistics_controller.rb +24 -0
  10. data/app/controllers/foreman_statistics/trends_controller.rb +58 -0
  11. data/app/helpers/foreman_statistics/trends_helper.rb +53 -0
  12. data/app/models/concerns/foreman_statistics/compute_resource_decorations.rb +9 -0
  13. data/app/models/concerns/foreman_statistics/environment_decorations.rb +9 -0
  14. data/app/models/concerns/foreman_statistics/general_setting_decorations.rb +17 -0
  15. data/app/models/concerns/foreman_statistics/hostgroup_decorations.rb +9 -0
  16. data/app/models/concerns/foreman_statistics/model_decorations.rb +9 -0
  17. data/app/models/concerns/foreman_statistics/operatingsystem_decorations.rb +9 -0
  18. data/app/models/concerns/foreman_statistics/setting_decorations.rb +9 -0
  19. data/app/models/foreman_statistics/fact_trend.rb +57 -0
  20. data/app/models/foreman_statistics/foreman_trend.rb +41 -0
  21. data/app/models/foreman_statistics/trend.rb +38 -0
  22. data/app/models/foreman_statistics/trend_counter.rb +11 -0
  23. data/app/services/foreman_statistics/statistics.rb +21 -0
  24. data/app/services/foreman_statistics/statistics/base.rb +39 -0
  25. data/app/services/foreman_statistics/statistics/count_facts.rb +17 -0
  26. data/app/services/foreman_statistics/statistics/count_hosts.rb +9 -0
  27. data/app/services/foreman_statistics/statistics/count_numerical_fact_pair.rb +43 -0
  28. data/app/services/foreman_statistics/statistics/count_puppet_classes.rb +23 -0
  29. data/app/services/foreman_statistics/trend_importer.rb +63 -0
  30. data/app/views/foreman_statistics/api/v2/trends/base.json.rabl +4 -0
  31. data/app/views/foreman_statistics/api/v2/trends/create.json.rabl +3 -0
  32. data/app/views/foreman_statistics/api/v2/trends/index.json.rabl +3 -0
  33. data/app/views/foreman_statistics/api/v2/trends/main.json.rabl +5 -0
  34. data/app/views/foreman_statistics/api/v2/trends/show.json.rabl +3 -0
  35. data/app/views/foreman_statistics/api/v2/trends/update.json.rabl +3 -0
  36. data/app/views/foreman_statistics/layouts/application_react.html.erb +16 -0
  37. data/app/views/foreman_statistics/trends/_empty_data.html.erb +7 -0
  38. data/app/views/foreman_statistics/trends/_fields.html.erb +7 -0
  39. data/app/views/foreman_statistics/trends/_form.html.erb +8 -0
  40. data/app/views/foreman_statistics/trends/_hosts.html.erb +13 -0
  41. data/app/views/foreman_statistics/trends/edit.html.erb +46 -0
  42. data/app/views/foreman_statistics/trends/index.html.erb +39 -0
  43. data/app/views/foreman_statistics/trends/new.html.erb +4 -0
  44. data/app/views/foreman_statistics/trends/show.html.erb +25 -0
  45. data/app/views/foreman_statistics/trends/welcome.html.erb +12 -0
  46. data/config/routes.rb +22 -0
  47. data/db/migrate/20200605153005_migrate_core_types.rb +15 -0
  48. data/db/migrate_foreman/20121012170851_create_trends.rb +25 -0
  49. data/db/migrate_foreman/20121012170936_create_trend_counters.rb +14 -0
  50. data/db/migrate_foreman/20150202094307_add_range_to_trend_counters.rb +6 -0
  51. data/db/migrate_foreman/20181031155025_add_trend_counter_created_at_unique_constraint.rb +9 -0
  52. data/lib/foreman_statistics.rb +4 -0
  53. data/lib/foreman_statistics/engine.rb +104 -0
  54. data/lib/foreman_statistics/version.rb +3 -0
  55. data/lib/tasks/foreman_statistics_tasks.rake +78 -0
  56. data/locale/Makefile +60 -0
  57. data/locale/en/foreman_statistics.po +19 -0
  58. data/locale/foreman_statistics.pot +19 -0
  59. data/locale/gemspec.rb +2 -0
  60. data/test/factories/foreman_statistics_factories.rb +68 -0
  61. data/test/fixtures/permissions.yml +26 -0
  62. data/test/fixtures/settings.yml +6 -0
  63. data/test/functional/foreman_statistics/api/v2/statistics_controller_test.rb +19 -0
  64. data/test/functional/foreman_statistics/api/v2/trends_controller_test.rb +74 -0
  65. data/test/functional/foreman_statistics/statistics_controller_test.rb +23 -0
  66. data/test/functional/foreman_statistics/trends_controller_test.rb +115 -0
  67. data/test/models/foreman_statistics/trend_counter_test.rb +10 -0
  68. data/test/models/foreman_statistics/trend_test.rb +22 -0
  69. data/test/test_plugin_helper.rb +6 -0
  70. data/test/unit/foreman_statistics/access_permissions_test.rb +16 -0
  71. data/test/unit/foreman_statistics/statistics_test.rb +82 -0
  72. data/test/unit/foreman_statistics_test.rb +11 -0
  73. data/test/unit/tasks/foreman_statistics_tasks_test.rb +205 -0
  74. metadata +199 -0
@@ -0,0 +1,57 @@
1
+ module ForemanStatistics
2
+ class FactTrend < Trend
3
+ validates :trendable_id, :presence => true, :uniqueness => { :scope => %i[trendable_type fact_value] }, :allow_blank => false
4
+
5
+ before_save :update_fact_name
6
+
7
+ def to_label
8
+ name.presence || fact_value || fact_name
9
+ end
10
+
11
+ def type_name
12
+ if fact_value.blank?
13
+ name.presence || fact_name
14
+ else
15
+ fact_name
16
+ end
17
+ end
18
+
19
+ def create_values
20
+ self.class.create_values(trendable_id)
21
+ end
22
+
23
+ def self.create_values(fact_name_id)
24
+ FactValue.select('fact_name_id, value').group(:fact_name_id, :value).where(:fact_name_id => fact_name_id).includes(:fact_name).map do |fact|
25
+ create(:trendable_type => 'FactName',
26
+ :trendable_id => fact.fact_name.id,
27
+ :fact_name => fact.fact_name.name,
28
+ :fact_value => fact.value,
29
+ :name => fact.value)
30
+ end
31
+ end
32
+
33
+ def destroy_values
34
+ ids = FactTrend.where(:trendable_id => trendable_id, :trendable_type => trendable_type).pluck(:id)
35
+ super(ids)
36
+ end
37
+
38
+ def values
39
+ return FactTrend.where(:id => self) if fact_value
40
+ FactTrend.has_value.where(:trendable_type => trendable_type, :trendable_id => trendable_id)
41
+ end
42
+
43
+ def self.model_name
44
+ Trend.model_name
45
+ end
46
+
47
+ def find_hosts
48
+ Host.joins(:fact_values).where(:fact_values => { :value => fact_value }).order(:name)
49
+ end
50
+
51
+ private
52
+
53
+ def update_fact_name
54
+ self.fact_name = FactName.find(trendable_id).name if trendable_id
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ module ForemanStatistics
2
+ class ForemanTrend < Trend
3
+ validates :trendable_id, :uniqueness => { :scope => :trendable_type }
4
+ validates :trendable_type, :presence => true
5
+
6
+ def to_label
7
+ trendable ? trendable.to_label : trendable_type
8
+ end
9
+
10
+ def type_name
11
+ trendable_type
12
+ end
13
+
14
+ def create_values
15
+ self.class.create_values(trendable_type)
16
+ end
17
+
18
+ def self.create_values(trendable_type)
19
+ trendable_type.constantize.all.map { |t| t.trends.create(:fact_value => t.to_label) }
20
+ end
21
+
22
+ def destroy_values
23
+ ids = ForemanTrend.where(:trendable_type => trendable_type).pluck(:id)
24
+ super(ids)
25
+ end
26
+
27
+ def values
28
+ return ForemanTrend.where(:id => self) if fact_value
29
+ ForemanTrend.has_value.where(:trendable_type => trendable_type)
30
+ end
31
+
32
+ def self.model_name
33
+ Trend.model_name
34
+ end
35
+
36
+ def find_hosts
37
+ return Host::Managed.none unless trendable
38
+ trendable.hosts.order(:name)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ module ForemanStatistics
2
+ class Trend < ApplicationRecord
3
+ self.table_name = 'trends'
4
+
5
+ validates_lengths_from_database
6
+ after_save :create_values, :if => ->(o) { o.fact_value.nil? }
7
+ after_destroy :destroy_values, :if => ->(o) { o.fact_value.nil? }
8
+
9
+ belongs_to :trendable, :polymorphic => true
10
+ has_many :trend_counters, :dependent => :destroy
11
+
12
+ scope :has_value, -> { where('fact_value IS NOT NULL').order('fact_value') }
13
+ scope :types, -> { where(:fact_value => nil) }
14
+
15
+ def to_param
16
+ Parameterizable.parameterize("#{id}-#{to_label}")
17
+ end
18
+
19
+ def self.title_name
20
+ 'label'.freeze
21
+ end
22
+
23
+ def self.humanize_class_name(_name = nil)
24
+ super('Trend')
25
+ end
26
+
27
+ def self.build_trend(trend_params = {})
28
+ trend_params[:trendable_type] == 'FactName' ? FactTrend.new(trend_params) : ForemanTrend.new(trend_params)
29
+ end
30
+
31
+ private
32
+
33
+ def destroy_values(ids = [])
34
+ TrendCounter.where(:trend_id => ids).delete_all
35
+ Trend.where(:id => ids).delete_all
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ module ForemanStatistics
2
+ class TrendCounter < ApplicationRecord
3
+ self.table_name = 'trend_counters'
4
+
5
+ belongs_to :trend
6
+ validates :count, :numericality => { :greater_than_or_equal_to => 0 }
7
+ validates :created_at, :uniqueness => { :scope => :trend_id }
8
+ default_scope -> { order(:created_at) }
9
+ scope :recent, ->(*args) { where('created_at > ?', (args.first || 30.days.ago)).order(:created_at) }
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ module ForemanStatistics
2
+ module Statistics
3
+ def self.charts(org_id, loc_id)
4
+ charts = [
5
+ CountHosts.new(:count_by => :operatingsystem, :title => 'OS Distribution', :search => 'os_title=~VAL~', :organization_id => org_id, :location_id => loc_id),
6
+ CountHosts.new(:count_by => :architecture, :title => _('Architecture Distribution'), :search => 'facts.architecture=~VAL~', :organization_id => org_id, :location_id => loc_id),
7
+ CountHosts.new(:count_by => :environment, :title => _('Environment Distribution'), :search => 'environment=~VAL~', :organization_id => org_id, :location_id => loc_id),
8
+ CountHosts.new(:count_by => :hostgroup, :title => _('Host Group Distribution'), :search => 'hostgroup_title=~VAL~', :organization_id => org_id, :location_id => loc_id),
9
+ CountHosts.new(:count_by => :compute_resource, :title => _('Compute Resource Distribution'), :search => 'compute_resource=~VAL~', :organization_id => org_id, :location_id => loc_id),
10
+ CountFacts.new(:count_by => :processorcount, :unit => Nn_('%s core', '%s cores'), :title => _('Number of CPUs'), :search => 'facts.processorcount=~VAL1~', :organization_id => org_id, :location_id => loc_id),
11
+ CountFacts.new(:count_by => :manufacturer, :title => _('Hardware'), :search => 'facts.manufacturer~~VAL~', :organization_id => org_id, :location_id => loc_id),
12
+ CountNumericalFactPair.new(:count_by => :memory, :title => _('Average Memory Usage'), :organization_id => org_id, :location_id => loc_id),
13
+ CountNumericalFactPair.new(:count_by => :swap, :title => _('Average Swap Usage'), :organization_id => org_id, :location_id => loc_id),
14
+ CountPuppetClasses.new(:id => :puppetclass, :title => _('Class Distribution'), :search => 'class=~VAL1~', :organization_id => org_id, :location_id => loc_id),
15
+ CountHosts.new(:count_by => :location, :title => _('Location Distribution'), :search => 'location=~VAL~', :organization_id => org_id, :location_id => loc_id),
16
+ CountHosts.new(:count_by => :organization, :title => _('Organization Distribution'), :search => 'organization=~VAL~', :organization_id => org_id, :location_id => loc_id)
17
+ ]
18
+ charts
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ module ForemanStatistics
2
+ module Statistics
3
+ class Base
4
+ attr_reader :title, :count_by, :url
5
+
6
+ def initialize(options = {})
7
+ @id = options[:id]
8
+ @title = options[:title]
9
+ @search = options[:search]
10
+ @count_by = options[:count_by]
11
+ @organization_id = options[:organization_id]
12
+ @location_id = options[:location_id]
13
+ @url = options[:url] || build_url
14
+ end
15
+
16
+ def calculate
17
+ raise NotImplementedError, "Method 'calculate' method needs to be implemented"
18
+ end
19
+
20
+ def id
21
+ @id || count_by.to_s
22
+ end
23
+
24
+ def search
25
+ Rails.application.routes.url_helpers.hosts_path(:search => @search)
26
+ end
27
+
28
+ def metadata
29
+ { :id => id, :title => title, :url => url, :search => search }
30
+ end
31
+
32
+ private
33
+
34
+ def build_url
35
+ ForemanStatistics::Engine.routes.url_helpers.statistic_path(id, :location_id => @location_id, :organization_id => @organization_id)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module ForemanStatistics
2
+ module Statistics
3
+ class CountFacts < Base
4
+ attr_reader :unit
5
+
6
+ def initialize(options = {})
7
+ super(options)
8
+ @count_by = @count_by.to_s
9
+ @unit = options[:unit]
10
+ end
11
+
12
+ def calculate
13
+ FactValue.authorized(:view_facts).my_facts.count_each(count_by, :unit => unit)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module ForemanStatistics
2
+ module Statistics
3
+ class CountHosts < Base
4
+ def calculate
5
+ Host.authorized(:view_hosts, Host).count_distribution(count_by)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ module ForemanStatistics
2
+ module Statistics
3
+ class CountNumericalFactPair < Base
4
+ attr_reader :total, :used
5
+
6
+ def initialize(options = {})
7
+ super(options)
8
+ if @count_by.empty?
9
+ raise(ArgumentError, 'Must provide :count_by option')
10
+ end
11
+ @count_by = @count_by.to_s
12
+ @total = options[:total] || 'size'
13
+ @used = options[:used] || 'free'
14
+ end
15
+
16
+ def calculate
17
+ mem_size = FactValue.authorized(:view_facts).my_facts.mem_average(total_name)
18
+ mem_free = FactValue.authorized(:view_facts).my_facts.mem_average(used_name)
19
+
20
+ [
21
+ {
22
+ :label => _('free memory'),
23
+ :data => mem_free
24
+ },
25
+ {
26
+ :label => _('used memory'),
27
+ :data => (mem_size - mem_free).round(2)
28
+ }
29
+ ]
30
+ end
31
+
32
+ private
33
+
34
+ def total_name
35
+ count_by + total
36
+ end
37
+
38
+ def used_name
39
+ count_by + used
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module ForemanStatistics
2
+ module Statistics
3
+ class CountPuppetClasses < Base
4
+ def initialize(options = {})
5
+ super(options)
6
+ if id.empty?
7
+ raise(ArgumentError, 'Must provide an :id or :count_by option')
8
+ end
9
+ end
10
+
11
+ def calculate
12
+ Puppetclass.authorized(:view_puppetclasses).map do |pc|
13
+ count = pc.hosts_count
14
+ next if count.zero?
15
+ {
16
+ :label => pc.to_label,
17
+ :data => count
18
+ }
19
+ end.compact
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ module ForemanStatistics
2
+ class TrendImporter
3
+ def self.update!
4
+ importer = new
5
+ importer.check_values
6
+ importer.update_trend_counters
7
+ importer.aggregate_counters
8
+ end
9
+
10
+ # Check for missing values
11
+ # Comparing a count prior to trying to recreating them all for efficiency sake
12
+ def check_values
13
+ ForemanTrend.types.pluck(:trendable_type).each do |trend_type|
14
+ changes = trend_type.constantize.pluck(:id) - ForemanTrend.has_value.where(:trendable_type => trend_type).pluck(:trendable_id)
15
+ ForemanTrend.create_values(trend_type) unless changes.empty?
16
+ end
17
+
18
+ FactTrend.types.pluck(:trendable_id).each do |fact_name_id|
19
+ changes = FactValue.where(:fact_name_id => fact_name_id).group(:value).pluck(:value) - FactTrend.has_value.where(:trendable_id => fact_name_id).pluck(:fact_value)
20
+ FactTrend.create_values(fact_name_id) unless changes.empty?
21
+ end
22
+ end
23
+
24
+ def update_trend_counters
25
+ timestamp = Time.now.utc
26
+ counter_hash = {}
27
+ Trend.types.each do |trend|
28
+ if trend.is_a? FactTrend
29
+ counter_hash[trend.trendable_id] = Host.joins(:fact_values).where(:fact_values => { :fact_name_id => trend.trendable_id }).group(:value).count
30
+ else
31
+ counter_hash[trend.trendable_type] = Host.group(trend.trendable_type.foreign_key.to_sym).count
32
+ end
33
+ end
34
+ Trend.has_value.each do |trend|
35
+ new_count = if trend.is_a? FactTrend
36
+ counter_hash[trend.trendable_id][trend.fact_value]
37
+ else
38
+ counter_hash[trend.trendable_type][trend.trendable_id]
39
+ end || 0
40
+
41
+ latest_counter = trend.trend_counters.order(:created_at).last
42
+ if latest_counter
43
+ latest_counter.interval_end = timestamp
44
+ latest_counter.save!
45
+ end
46
+
47
+ next unless self.class.should_create_counter?(latest_counter, new_count, timestamp)
48
+
49
+ trend.trend_counters.create! :count => new_count,
50
+ :created_at => timestamp,
51
+ :interval_start => timestamp
52
+ end
53
+ end
54
+
55
+ def self.should_create_counter?(latest_counter, new_count, _timestamp)
56
+ return true if latest_counter.nil?
57
+
58
+ latest_counter.count != new_count
59
+ end
60
+
61
+ def aggregate_counters; end
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ object @trend
2
+
3
+ attributes :id, :trendable_type, :trendable_id, :fact_name, :type, :name
4
+ attribute to_label: :label
@@ -0,0 +1,3 @@
1
+ object @trend
2
+
3
+ extends 'foreman_statistics/api/v2/trends/show'
@@ -0,0 +1,3 @@
1
+ collection @trends
2
+
3
+ extends 'foreman_statistics/api/v2/trends/main'
@@ -0,0 +1,5 @@
1
+ object @trends
2
+
3
+ extends 'foreman_statistics/api/v2/trends/base'
4
+
5
+ attributes :created_at, :updated_at
@@ -0,0 +1,3 @@
1
+ object @trend
2
+
3
+ extends 'foreman_statistics/api/v2/trends/main'
@@ -0,0 +1,3 @@
1
+ object @trend
2
+
3
+ extends 'foreman_statistics/api/v2/trends/show'
@@ -0,0 +1,16 @@
1
+
2
+ <% content_for(:javascripts) do %>
3
+ <%= webpacked_plugins_js_for :foreman_statistics %>
4
+ <% end %>
5
+ <% content_for(:stylesheets) do %>
6
+ <%= webpacked_plugins_css_for :foreman_statistics %>
7
+ <% end %>
8
+
9
+ <% content_for(:content) do %>
10
+ <%= notifications %>
11
+ <div id="organization-id" data-id="<%= Organization.current.id if Organization.current %>" ></div>
12
+ <div id="user-id" data-id="<%= User.current.id if User.current %>" ></div>
13
+ <div id="foremanStatisticsRoot"></div>
14
+ <% end %>
15
+ <%= render file: "layouts/base" %>
16
+ <%= mount_react_component('ForemanStatistics', '#foremanStatisticsRoot') %>
@@ -0,0 +1,7 @@
1
+ <% title(_("Trends for %s") % trend_title(@trend)) %>
2
+ <div class="row">
3
+ <div class="stats-well col-md-12">
4
+ <p><strong><%= _('No data for this trend.') %></strong></p>
5
+ <div><%= (_("Is the cron job that executes %s enabled?") % "<span class='black'>foreman-rake foreman_statistics:trends:counter</span>").html_safe %></div>
6
+ </div>
7
+ </div>
@@ -0,0 +1,7 @@
1
+ <%# base_errors_for f %>
2
+ <td>
3
+ <%= trend.fact_value %>
4
+ </td>
5
+ <td>
6
+ <%= f.text_field :name %>
7
+ </td>