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.
- 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>
|