foreman_statistics 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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>