foreman_statistics 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 +40 -0
- data/Rakefile +47 -0
- data/app/controllers/concerns/foreman_statistics/parameters/trend.rb +20 -0
- data/app/controllers/foreman_statistics/api/v2/statistics_controller.rb +33 -0
- data/app/controllers/foreman_statistics/api/v2/trends_controller.rb +58 -0
- data/app/controllers/foreman_statistics/react_controller.rb +19 -0
- data/app/controllers/foreman_statistics/statistics_controller.rb +24 -0
- data/app/controllers/foreman_statistics/trends_controller.rb +58 -0
- data/app/helpers/foreman_statistics/trends_helper.rb +53 -0
- data/app/models/concerns/foreman_statistics/compute_resource_decorations.rb +9 -0
- data/app/models/concerns/foreman_statistics/environment_decorations.rb +9 -0
- data/app/models/concerns/foreman_statistics/general_setting_decorations.rb +17 -0
- data/app/models/concerns/foreman_statistics/hostgroup_decorations.rb +9 -0
- data/app/models/concerns/foreman_statistics/model_decorations.rb +9 -0
- data/app/models/concerns/foreman_statistics/operatingsystem_decorations.rb +9 -0
- data/app/models/concerns/foreman_statistics/setting_decorations.rb +9 -0
- data/app/models/foreman_statistics/fact_trend.rb +57 -0
- data/app/models/foreman_statistics/foreman_trend.rb +41 -0
- data/app/models/foreman_statistics/trend.rb +38 -0
- data/app/models/foreman_statistics/trend_counter.rb +11 -0
- data/app/services/foreman_statistics/statistics.rb +21 -0
- data/app/services/foreman_statistics/statistics/base.rb +39 -0
- data/app/services/foreman_statistics/statistics/count_facts.rb +17 -0
- data/app/services/foreman_statistics/statistics/count_hosts.rb +9 -0
- data/app/services/foreman_statistics/statistics/count_numerical_fact_pair.rb +43 -0
- data/app/services/foreman_statistics/statistics/count_puppet_classes.rb +23 -0
- data/app/services/foreman_statistics/trend_importer.rb +63 -0
- data/app/views/foreman_statistics/api/v2/trends/base.json.rabl +4 -0
- data/app/views/foreman_statistics/api/v2/trends/create.json.rabl +3 -0
- data/app/views/foreman_statistics/api/v2/trends/index.json.rabl +3 -0
- data/app/views/foreman_statistics/api/v2/trends/main.json.rabl +5 -0
- data/app/views/foreman_statistics/api/v2/trends/show.json.rabl +3 -0
- data/app/views/foreman_statistics/api/v2/trends/update.json.rabl +3 -0
- data/app/views/foreman_statistics/layouts/application_react.html.erb +16 -0
- data/app/views/foreman_statistics/trends/_empty_data.html.erb +7 -0
- data/app/views/foreman_statistics/trends/_fields.html.erb +7 -0
- data/app/views/foreman_statistics/trends/_form.html.erb +8 -0
- data/app/views/foreman_statistics/trends/_hosts.html.erb +13 -0
- data/app/views/foreman_statistics/trends/edit.html.erb +46 -0
- data/app/views/foreman_statistics/trends/index.html.erb +39 -0
- data/app/views/foreman_statistics/trends/new.html.erb +4 -0
- data/app/views/foreman_statistics/trends/show.html.erb +25 -0
- data/app/views/foreman_statistics/trends/welcome.html.erb +12 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20200605153005_migrate_core_types.rb +15 -0
- data/db/migrate_foreman/20121012170851_create_trends.rb +25 -0
- data/db/migrate_foreman/20121012170936_create_trend_counters.rb +14 -0
- data/db/migrate_foreman/20150202094307_add_range_to_trend_counters.rb +6 -0
- data/db/migrate_foreman/20181031155025_add_trend_counter_created_at_unique_constraint.rb +9 -0
- data/lib/foreman_statistics.rb +4 -0
- data/lib/foreman_statistics/engine.rb +104 -0
- data/lib/foreman_statistics/version.rb +3 -0
- data/lib/tasks/foreman_statistics_tasks.rake +78 -0
- data/locale/Makefile +60 -0
- data/locale/en/foreman_statistics.po +19 -0
- data/locale/foreman_statistics.pot +19 -0
- data/locale/gemspec.rb +2 -0
- data/test/factories/foreman_statistics_factories.rb +68 -0
- data/test/fixtures/permissions.yml +26 -0
- data/test/fixtures/settings.yml +6 -0
- data/test/functional/foreman_statistics/api/v2/statistics_controller_test.rb +19 -0
- data/test/functional/foreman_statistics/api/v2/trends_controller_test.rb +74 -0
- data/test/functional/foreman_statistics/statistics_controller_test.rb +23 -0
- data/test/functional/foreman_statistics/trends_controller_test.rb +115 -0
- data/test/models/foreman_statistics/trend_counter_test.rb +10 -0
- data/test/models/foreman_statistics/trend_test.rb +22 -0
- data/test/test_plugin_helper.rb +6 -0
- data/test/unit/foreman_statistics/access_permissions_test.rb +16 -0
- data/test/unit/foreman_statistics/statistics_test.rb +82 -0
- data/test/unit/foreman_statistics_test.rb +11 -0
- data/test/unit/tasks/foreman_statistics_tasks_test.rb +205 -0
- 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,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,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>
|