foreman_host_reports 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/helpers/foreman_host_reports/reports_dashboard_helper.rb +79 -0
- data/app/models/concerns/foreman_host_reports/host_extensions.rb +1 -0
- data/app/views/dashboard/_host_reports_status_chart_widget.html.erb +13 -0
- data/app/views/dashboard/_host_reports_status_links.html.erb +5 -0
- data/app/views/dashboard/_host_reports_status_widget.html.erb +11 -0
- data/app/views/dashboard/_host_reports_widget.html.erb +29 -0
- data/lib/foreman_host_reports/engine.rb +13 -0
- data/lib/foreman_host_reports/version.rb +1 -1
- data/lib/tasks/migrate.rake +235 -0
- data/package.json +5 -8
- data/test/controllers/api/v2/host_reports_controller_test.rb +0 -27
- data/test/factories/foreman_host_reports_factories.rb +12 -0
- data/test/unit/dashboard_test.rb +65 -0
- data/webpack/fills.js +6 -0
- data/webpack/src/components/ReportsTab/StatusToggleGroup.js +56 -0
- data/webpack/src/components/ReportsTab/helpers.js +1 -1
- data/webpack/src/components/ReportsTab/index.js +36 -11
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ef176b8d987ed56aa02950203e7cfdfc880d050be4701377c15aa8bcd20559a
|
4
|
+
data.tar.gz: 58a23f1ad805649d1dafffa7dcfe2af89c733380476588ae0b31df34b6502c68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64aa7da0150a3857d6f2a6fbefd37d9ee60fc7cdbbf741ee39671708b8e487100616d3632aa8622b649f6c3ab17232a793353a521058debd6a5511565a0f5f90
|
7
|
+
data.tar.gz: 50876e1b5b656de267b8580e3503f2db25c9c954a5cfb333e0b4fc4d0d376fb9aeb18f7309426c599d1173e847940166660469826e2e53b4cc3464cfe0b37389
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ForemanHostReports
|
2
|
+
module ReportsDashboardHelper
|
3
|
+
def host_reports_searchable_links(name, search, counter, format)
|
4
|
+
content_tag :li, :style => "margin-bottom: 5px" do
|
5
|
+
content_tag(:span, sanitize(' '), :class => 'label', :style => "background-color: #{host_reports_report_color[counter]}") +
|
6
|
+
sanitize(' ') +
|
7
|
+
link_to(name, hosts_path(:search => search), :class => "dashboard-links") +
|
8
|
+
content_tag(:span, send(counter, format), class: 'pull-right')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def host_reports_report_color
|
13
|
+
{
|
14
|
+
:change_hosts => "#4572A7",
|
15
|
+
:failure_hosts => "#AA4643",
|
16
|
+
:nochange_hosts => "#DB843D",
|
17
|
+
:disabled_hosts => "#92A8CD",
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def host_reports_get_overview(options = {})
|
22
|
+
format = options[:format]
|
23
|
+
state_labels = {
|
24
|
+
change_hosts: _('Change'),
|
25
|
+
failure_hosts: _('Failure'),
|
26
|
+
nochange_hosts: _('No Change'),
|
27
|
+
disabled_hosts: _('Disabled alerts'),
|
28
|
+
}
|
29
|
+
counter = {}
|
30
|
+
total = 0
|
31
|
+
data = state_labels.map do |key, label|
|
32
|
+
counter.store(key, send(key, format))
|
33
|
+
total = counter[key] + total
|
34
|
+
[label, counter[key], host_reports_report_color[key]]
|
35
|
+
end
|
36
|
+
{
|
37
|
+
data: data,
|
38
|
+
searchUrl: hosts_path(search: '~VAL~'),
|
39
|
+
title: { primary: _("#{(counter[:failure_hosts].fdiv(total) * 100).to_i}%"), secondary: state_labels[:failure_hosts] },
|
40
|
+
searchFilters: state_labels.each_with_object({}) do |(key, filter), filters|
|
41
|
+
filters[filter] = counter[key]
|
42
|
+
end,
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def latest_reports
|
47
|
+
HostReport.authorized(:view_host_reports).my_reports
|
48
|
+
.reorder(:reported_at => :desc)
|
49
|
+
.limit(9)
|
50
|
+
.preload(:host)
|
51
|
+
end
|
52
|
+
|
53
|
+
def latest_reports?
|
54
|
+
latest_reports.limit(1).present?
|
55
|
+
end
|
56
|
+
|
57
|
+
def host_reports_translated_header(shortname, longname)
|
58
|
+
"<th style='width:85px; class='ca'><span class='small' title='' data-original-title='#{longname}'>#{shortname}</span></th>"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def disabled_hosts(_format = nil)
|
64
|
+
Host.authorized(:view_hosts, Host).where(:enabled => false).count
|
65
|
+
end
|
66
|
+
|
67
|
+
def change_hosts(format)
|
68
|
+
HostReport.authorized(:view_host_reports).my_reports.where("format = ? and change > 0", HostReport.formats[format]).reorder('').group(:host_id).maximum(:id).count
|
69
|
+
end
|
70
|
+
|
71
|
+
def nochange_hosts(format)
|
72
|
+
HostReport.authorized(:view_host_reports).my_reports.where("format = ? and nochange > 0", HostReport.formats[format]).reorder('').group(:host_id).maximum(:id).count
|
73
|
+
end
|
74
|
+
|
75
|
+
def failure_hosts(format)
|
76
|
+
HostReport.authorized(:view_host_reports).my_reports.where("format = ? and failure > 0", HostReport.formats[format]).reorder('').group(:host_id).maximum(:id).count
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -10,6 +10,7 @@ module ForemanHostReports
|
|
10
10
|
scoped_search relation: :host_reports, on: :change, rename: :report_changes, only_explicit: true
|
11
11
|
scoped_search relation: :host_reports, on: :nochange, rename: :report_nochanges, only_explicit: true
|
12
12
|
scoped_search relation: :host_reports, on: :failure, rename: :report_failures, only_explicit: true
|
13
|
+
scoped_search relation: :host_reports, on: :format, rename: :report_format, only_explicit: true, complete_value: { plain: 0, puppet: 1, ansible: 2 }
|
13
14
|
end
|
14
15
|
end
|
15
16
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<%
|
2
|
+
format = settings[:format]
|
3
|
+
%>
|
4
|
+
<h4 class="header">
|
5
|
+
<% if format %>
|
6
|
+
<%= _('%s Host Reports Chart') % format %>
|
7
|
+
<% else %>
|
8
|
+
<%= _('All Host Reports Chart') %>
|
9
|
+
<% end %>
|
10
|
+
</h4>
|
11
|
+
<div class="host-configuration-chart">
|
12
|
+
<%= react_component('DonutChart', host_reports_get_overview(format: format.downcase))%>
|
13
|
+
</div>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
|
2
|
+
<%= host_reports_searchable_links _('Hosts with changes'), "report_changes > 0 and report_format = #{format}", :change_hosts, format%>
|
3
|
+
<%= host_reports_searchable_links _('Hosts without changes'), "report_nochanges > 0 and report_format = #{format}", :nochange_hosts, format %>
|
4
|
+
<%= host_reports_searchable_links _('Hosts with failures'), "report_failures > 0 and report_format = #{format}" , :failure_hosts, format %>
|
5
|
+
<%= host_reports_searchable_links _('Hosts with disabled alerts'), "status.enabled = false" , :disabled_hosts, format %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<h4 class="header">
|
2
|
+
<% if (format = settings[:format]) %>
|
3
|
+
<%= _('%s Host Reports') % settings[:format] %>
|
4
|
+
<% else %>
|
5
|
+
<%= _('All Host Reports %s') % disabled_hosts%>
|
6
|
+
<% end %>
|
7
|
+
</h4>
|
8
|
+
<ul>
|
9
|
+
<%= render "dashboard/host_reports_status_links", format: format.downcase %>
|
10
|
+
<h4 class="total"><%= _("Total Hosts: %s") % Host.all.count %></h4>
|
11
|
+
</ul>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<h4 class="header">
|
2
|
+
<%= _("Latest Host Reports") %>
|
3
|
+
</h4>
|
4
|
+
|
5
|
+
<% if latest_reports? %>
|
6
|
+
<table class="<%= table_css_classes 'table-fixed reports-table' %>">
|
7
|
+
<thead>
|
8
|
+
<tr>
|
9
|
+
<%= string = "<th>#{_('Host')}</th>"
|
10
|
+
string += host_reports_translated_header(s_('Changed'), _('Changed'))
|
11
|
+
string += host_reports_translated_header(s_('Unchanged'), _('Unchanged'))
|
12
|
+
string += host_reports_translated_header(s_('Failed'), _('Failed'))
|
13
|
+
string.html_safe %>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tbody>
|
17
|
+
<% latest_reports.each do |report| %>
|
18
|
+
<tr>
|
19
|
+
<td class='ellipsis'><%= link_to report.host, host_reports_host_path(report.host.id) %></td>
|
20
|
+
<td class="ca"><%= report_event_column(report.change, "label-info") %></td>
|
21
|
+
<td class="ca"><%= report_event_column(report.nochange, "label-warning") %></td>
|
22
|
+
<td class="ca"><%= report_event_column(report.failure, "label-danger") %></td>
|
23
|
+
</tr>
|
24
|
+
<% end %>
|
25
|
+
</tbody>
|
26
|
+
</table>
|
27
|
+
<% else %>
|
28
|
+
<p class="ca"><%= _("No interesting reports received in the last week") %></p>
|
29
|
+
<% end %>
|
@@ -44,6 +44,19 @@ module ForemanHostReports
|
|
44
44
|
add_histogram_telemetry(:host_report_create_keywords, 'Time spent processing keywords (ms)')
|
45
45
|
add_histogram_telemetry(:host_report_create_refresh, 'Time spent processing status refresh (ms)')
|
46
46
|
add_histogram_telemetry(:host_report_create, 'Time spent saving record (ms)')
|
47
|
+
|
48
|
+
# add dashboard widget
|
49
|
+
in_to_prepare do
|
50
|
+
HostReport.formats.each do |format, _|
|
51
|
+
if format != 'plain' && (format = format.capitalize)
|
52
|
+
widget 'host_reports_status_chart_widget', :name => N_(" %s Host Reports Chart ") % format, :sizex => 4, :sizey => 1, settings: { format: format }
|
53
|
+
widget 'host_reports_status_widget', :name => N_(" %s Host Reports ") % format, :sizex => 4, :sizey => 1, settings: { format: format }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
::DashboardController.helper ForemanHostReports::ReportsDashboardHelper
|
58
|
+
widget 'host_reports_widget', :name => N_('Host Reports'), :sizex => 6, :sizey => 1
|
59
|
+
end
|
47
60
|
end
|
48
61
|
end
|
49
62
|
|
@@ -0,0 +1,235 @@
|
|
1
|
+
PUPPET_LOG_LEVELS = %w[debug info notice warning err alert emerg crit].freeze
|
2
|
+
reports_migrate_running = true
|
3
|
+
|
4
|
+
namespace :host_reports do
|
5
|
+
def detect_puppet_keywords(status, logs)
|
6
|
+
result = ["Migrated"]
|
7
|
+
# from statuses
|
8
|
+
result << "PuppetFailed" if status["failed"]&.positive?
|
9
|
+
result << "PuppetFailedToRestart" if status["failed_restarts"]&.positive?
|
10
|
+
result << "PuppetCorrectiveChange" if status["corrective_change"]&.positive?
|
11
|
+
result << "PuppetSkipped" if status["skipped"]&.positive?
|
12
|
+
result << "PuppetRestarted" if status["restarted"]&.positive?
|
13
|
+
result << "PuppetScheduled" if status["scheduled"]&.positive?
|
14
|
+
result << "PuppetOutOfSync" if status["out_of_sync"]&.positive?
|
15
|
+
# from logs
|
16
|
+
logs.each do |level, resource, _message|
|
17
|
+
result << "PuppetFailed:#{resource}" if level == "err" && resource != "Puppet"
|
18
|
+
end
|
19
|
+
result.uniq
|
20
|
+
end
|
21
|
+
|
22
|
+
# Puppet status cannot be directly mapped, let's create unique migration-only keywords.
|
23
|
+
# See: https://community.theforeman.org/t/new-config-report-summary-columns/26531
|
24
|
+
def detect_ansible_keywords(status)
|
25
|
+
result = ["Migrated"]
|
26
|
+
# from statuses
|
27
|
+
result << "AnsibleMigrate:Applied" if status["applied"]&.positive?
|
28
|
+
result << "AnsibleMigrate:Restarted" if status["restarted"]&.positive?
|
29
|
+
result << "AnsibleMigrate:Failed" if status["failed"]&.positive?
|
30
|
+
result << "AnsibleMigrate:FailedRestarts" if status["failed_restarts"]&.positive?
|
31
|
+
result << "AnsibleMigrate:Skipped" if status["skipped"]&.positive?
|
32
|
+
result << "AnsibleMigrate:Pending" if status["pending"]&.positive?
|
33
|
+
result.uniq
|
34
|
+
end
|
35
|
+
|
36
|
+
def puppet_metrics(metrics)
|
37
|
+
return [0, 0, 0] if metrics.empty?
|
38
|
+
change = metrics.dig("events", "success") || 0
|
39
|
+
failure = metrics.dig("events", "failure") || 0
|
40
|
+
total = metrics.dig("events", "total") || 0
|
41
|
+
nochange = total - change - failure
|
42
|
+
[change, failure, nochange]
|
43
|
+
end
|
44
|
+
|
45
|
+
def summary(origin, metrics, status)
|
46
|
+
change, failure, nochange = 0
|
47
|
+
case origin
|
48
|
+
when "Puppet"
|
49
|
+
change, failure, nochange = puppet_metrics(metrics)
|
50
|
+
when "Ansible"
|
51
|
+
# There is not enough data to construct the summary, it is not possible to
|
52
|
+
# efficiently map ansible status values to the new format. See the discussion
|
53
|
+
# at: https://community.theforeman.org/t/new-config-report-summary-columns/26531
|
54
|
+
failure = status["failed"]
|
55
|
+
change = status["applied"]
|
56
|
+
nochange = status["skipped"]
|
57
|
+
end
|
58
|
+
|
59
|
+
{
|
60
|
+
foreman: {
|
61
|
+
change: change,
|
62
|
+
nochange: nochange,
|
63
|
+
failure: failure,
|
64
|
+
},
|
65
|
+
native: metrics[:resources] || {},
|
66
|
+
legacy_status: status || {},
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_body(format, metrics, reported_at, _status, host, keywords, summary)
|
71
|
+
{
|
72
|
+
version: 1,
|
73
|
+
format: format,
|
74
|
+
migrated: true,
|
75
|
+
host: host.name,
|
76
|
+
reported_at: reported_at,
|
77
|
+
keywords: keywords,
|
78
|
+
summary: summary,
|
79
|
+
# metrics cannot be migrated because Foreman stores them in its own way and
|
80
|
+
# the new host format uses puppet native version
|
81
|
+
metrics: {
|
82
|
+
resources: { values: [] },
|
83
|
+
time: { values: [] },
|
84
|
+
changes: { values: [] },
|
85
|
+
events: { values: [] },
|
86
|
+
},
|
87
|
+
# keep the legacy metrics in the body in case we reconsider and transform it later
|
88
|
+
legacy_metrics: metrics,
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_report(host_id, origin, body, report_keyword_ids)
|
93
|
+
{
|
94
|
+
host_id: host_id,
|
95
|
+
proxy_id: nil,
|
96
|
+
format: origin,
|
97
|
+
reported_at: body[:reported_at],
|
98
|
+
body: body.to_json,
|
99
|
+
change: body.dig(:summary, :foreman, :change),
|
100
|
+
nochange: body.dig(:summary, :foreman, :nochange),
|
101
|
+
failure: body.dig(:summary, :foreman, :failure),
|
102
|
+
report_keyword_ids: report_keyword_ids,
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_puppet_logs(id, log_object)
|
107
|
+
logs = [["debug", "migration", "Report migrated from legacy report ID=#{id} at #{Time.now.utc}"]]
|
108
|
+
log_object.includes(:message, :source).find_each do |log|
|
109
|
+
logs << [PUPPET_LOG_LEVELS[log.level_id] || 'unknown', log.source.value, log.message.value]
|
110
|
+
end
|
111
|
+
logs
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_ansible_result(msg, level, result = {}, task = {})
|
115
|
+
{
|
116
|
+
failed: false,
|
117
|
+
level: level,
|
118
|
+
friendly_message: msg,
|
119
|
+
result: result,
|
120
|
+
task: task,
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_ansible_results(id, log_object)
|
125
|
+
results = []
|
126
|
+
results << create_ansible_result("Report migrated from legacy report ID=#{id} at #{Time.now.utc}", "info")
|
127
|
+
log_object.includes(:message, :source).find_each do |log|
|
128
|
+
lvl = PUPPET_LOG_LEVELS[log.level_id] || 'unknown'
|
129
|
+
msg = begin
|
130
|
+
JSON.parse(log.message.value)
|
131
|
+
rescue StandardError
|
132
|
+
log.message.value
|
133
|
+
end
|
134
|
+
results << create_ansible_result(log.source.value, lvl, msg)
|
135
|
+
end
|
136
|
+
results
|
137
|
+
end
|
138
|
+
|
139
|
+
desc <<-END_DESC
|
140
|
+
Migrates Foreman Configuration Reports to the new Host Reports format.
|
141
|
+
Does not delete legacy reports, can be iterrupted at any time.
|
142
|
+
Accepts from_date option (older reports will be ignored) and from_id,
|
143
|
+
primary key (ID) to start migration from which can be used to resume
|
144
|
+
previously stopped migration. Example:
|
145
|
+
|
146
|
+
foreman-rake host_reports:migrate from_date=2021-01-01 from_id=1234567
|
147
|
+
END_DESC
|
148
|
+
task :migrate => :environment do
|
149
|
+
Rails.logger.level = Logger::ERROR
|
150
|
+
Foreman::Logging.logger('permissions').level = Logger::ERROR
|
151
|
+
Foreman::Logging.logger('audit').level = Logger::ERROR
|
152
|
+
Signal.trap("INT") do
|
153
|
+
reports_migrate_running = false
|
154
|
+
end
|
155
|
+
Signal.trap("TERM") do
|
156
|
+
reports_migrate_running = false
|
157
|
+
end
|
158
|
+
|
159
|
+
from_id = (ENV['from_id'] || '0').to_i
|
160
|
+
from_date = ENV['from_date'] || '1980-01-15'
|
161
|
+
report_count = ConfigReport.unscoped.where("id >= ? and reported_at >= ?", from_id, from_date).count
|
162
|
+
print_each = 1 + (report_count / 100).to_i
|
163
|
+
puts "Starting, #{report_count} report(s) left"
|
164
|
+
counter = 0
|
165
|
+
ConfigReport.unscoped.all.where("id >= ? and reported_at >= ?", from_id, from_date).find_each do |r|
|
166
|
+
raise("Interrupted") unless reports_migrate_running
|
167
|
+
counter += 1
|
168
|
+
puts("Processing report #{counter} out of #{report_count} reports") if (counter % print_each).zero?
|
169
|
+
case r.origin
|
170
|
+
when "Puppet"
|
171
|
+
logs = create_puppet_logs(r.id, r.logs)
|
172
|
+
keywords = detect_puppet_keywords(r.status, logs)
|
173
|
+
when "Ansible"
|
174
|
+
results = create_ansible_results(r.id, r.logs)
|
175
|
+
keywords = detect_ansible_keywords(r.status)
|
176
|
+
end
|
177
|
+
if keywords.present?
|
178
|
+
keywords_to_insert = keywords.each_with_object([]) do |n, ks|
|
179
|
+
ks << { name: n }
|
180
|
+
end
|
181
|
+
ReportKeyword.upsert_all(keywords_to_insert, unique_by: :name)
|
182
|
+
report_keyword_ids = ReportKeyword.where(name: keywords).distinct.pluck(:id)
|
183
|
+
end
|
184
|
+
summary = summary(r.origin, r.metrics, r.status)
|
185
|
+
body = create_body(r.origin&.downcase, r.metrics, r.reported_at, r.status, r.host, report_keyword_ids, summary)
|
186
|
+
case r.origin
|
187
|
+
when "Puppet"
|
188
|
+
body[:logs] = logs
|
189
|
+
when "Ansible"
|
190
|
+
body[:results] = results
|
191
|
+
end
|
192
|
+
origin = r.origin.downcase
|
193
|
+
User.without_auditing do
|
194
|
+
User.as_anonymous_admin do
|
195
|
+
HostReport.create!(build_report(r.host_id, origin, body, report_keyword_ids))
|
196
|
+
end
|
197
|
+
end
|
198
|
+
rescue StandardError => e
|
199
|
+
puts "Error when processing report ID=#{r.id}"
|
200
|
+
puts r.inspect
|
201
|
+
puts "To resume the process:\n\n***\n\nforeman-rake host_reports:migrate from_id=#{r.id} from_date=#{from_date}\n\n***\n\n"
|
202
|
+
raise e
|
203
|
+
end
|
204
|
+
puts "\n\nALL DONE!\n\nCheck the migrated reports in Monitor - Host Reports first"
|
205
|
+
puts "and when ready, expire old configuration reports with:\n\n"
|
206
|
+
puts " rake reports:expire report_type=config_report days=0\n\n"
|
207
|
+
puts "Report expiration is slow, if you don't use OpenSCAP plugin, then"
|
208
|
+
puts "truncate the following tables in the foreman database"
|
209
|
+
puts "for a quick delete (this will remove also OpenSCAP reports):\n\n"
|
210
|
+
puts " truncate logs, messages, resources, reports;\n\n"
|
211
|
+
puts "Reclaim postgres database space via VACUUM function in any case."
|
212
|
+
puts "If migration was not successful, truncate tables host_reports and"
|
213
|
+
puts "report_keywords and start over.\n"
|
214
|
+
puts "Optionally, refresh host statuses with:\n\n"
|
215
|
+
puts " foreman-rake host_reports:refresh\n\n"
|
216
|
+
end
|
217
|
+
|
218
|
+
desc <<-END_DESC
|
219
|
+
Host status information can be incorrect until new report is received.
|
220
|
+
This task refreshes all host statuses and global statuses.
|
221
|
+
END_DESC
|
222
|
+
task :refresh => :environment do
|
223
|
+
Rails.logger.level = Logger::ERROR
|
224
|
+
Foreman::Logging.logger('permissions').level = Logger::ERROR
|
225
|
+
Foreman::Logging.logger('audit').level = Logger::ERROR
|
226
|
+
User.without_auditing do
|
227
|
+
User.as_anonymous_admin do
|
228
|
+
Host.unscoped.all.find_each do |h|
|
229
|
+
h.refresh_statuses
|
230
|
+
h.refresh_global_status
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
data/package.json
CHANGED
@@ -21,20 +21,17 @@
|
|
21
21
|
"url": "http://projects.theforeman.org/projects/foreman_host_reports/issues"
|
22
22
|
},
|
23
23
|
"peerDependencies": {
|
24
|
-
"@theforeman/vendor": "
|
24
|
+
"@theforeman/vendor": ">= 0"
|
25
25
|
},
|
26
26
|
"dependencies": {
|
27
27
|
"react-json-tree": "^0.11.0"
|
28
28
|
},
|
29
29
|
"devDependencies": {
|
30
30
|
"@babel/core": "^7.7.0",
|
31
|
-
"@
|
32
|
-
"@theforeman/
|
33
|
-
"@theforeman/
|
34
|
-
"@theforeman/
|
35
|
-
"@theforeman/stories": "^8.15.0",
|
36
|
-
"@theforeman/test": "^8.15.0",
|
37
|
-
"@theforeman/vendor-dev": "^8.15.0",
|
31
|
+
"@theforeman/builder": ">= 0",
|
32
|
+
"@theforeman/eslint-plugin-foreman": ">= 0",
|
33
|
+
"@theforeman/stories": ">= 0",
|
34
|
+
"@theforeman/test": ">= 0",
|
38
35
|
"babel-eslint": "^10.0.3",
|
39
36
|
"eslint": "^6.7.2",
|
40
37
|
"prettier": "^1.19.1",
|
@@ -167,33 +167,6 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
|
|
167
167
|
}
|
168
168
|
assert_response :forbidden
|
169
169
|
end
|
170
|
-
|
171
|
-
test 'when "require_ssl" is true, HTTP requests should not be able to create a report' do
|
172
|
-
Setting[:restrict_registered_smart_proxies] = true
|
173
|
-
SETTINGS[:require_ssl] = true
|
174
|
-
|
175
|
-
Resolv.any_instance.stubs(:getnames).returns(['else.where'])
|
176
|
-
post :create, params: {
|
177
|
-
host_report: {
|
178
|
-
host: host.name, body: report_body, reported_at: Time.current,
|
179
|
-
change: 1, nochange: 2, failure: 3
|
180
|
-
},
|
181
|
-
}
|
182
|
-
assert_response :redirect
|
183
|
-
end
|
184
|
-
|
185
|
-
test 'when "require_ssl" is false, HTTP requests should be able to create reports' do
|
186
|
-
Setting[:restrict_registered_smart_proxies] = true
|
187
|
-
SETTINGS[:require_ssl] = false
|
188
|
-
|
189
|
-
post :create, params: {
|
190
|
-
host_report: {
|
191
|
-
host: host.name, body: report_body, reported_at: Time.current,
|
192
|
-
change: 1, nochange: 2, failure: 3
|
193
|
-
},
|
194
|
-
}
|
195
|
-
assert_response :created
|
196
|
-
end
|
197
170
|
end
|
198
171
|
|
199
172
|
test 'should get index' do
|
@@ -17,6 +17,18 @@ FactoryBot.define do
|
|
17
17
|
format { 'ansible' }
|
18
18
|
end
|
19
19
|
|
20
|
+
trait :with_failure do
|
21
|
+
failure { 1 }
|
22
|
+
end
|
23
|
+
|
24
|
+
trait :with_change do
|
25
|
+
change { 1 }
|
26
|
+
end
|
27
|
+
|
28
|
+
trait :with_nochange do
|
29
|
+
nochange { 1 }
|
30
|
+
end
|
31
|
+
|
20
32
|
trait :with_keyword do
|
21
33
|
transient do
|
22
34
|
name { 'HasError' }
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
class DashboardTest < ActiveSupport::TestCase
|
4
|
+
include ForemanHostReports::ReportsDashboardHelper
|
5
|
+
|
6
|
+
let(:ansible_report_with_change) { FactoryBot.create(:host_report, :ansible_format, :with_change) }
|
7
|
+
let(:ansible_report_with_nochange) { FactoryBot.create(:host_report, :ansible_format, :with_nochange) }
|
8
|
+
let(:ansible_report_with_failure) { FactoryBot.create(:host_report, :ansible_format, :with_failure) }
|
9
|
+
|
10
|
+
let(:puppet_report_with_change) { FactoryBot.create(:host_report, :puppet_format, :with_change) }
|
11
|
+
let(:puppet_report_with_nochange) { FactoryBot.create(:host_report, :puppet_format, :with_nochange) }
|
12
|
+
let(:puppet_report_with_failure) { FactoryBot.create(:host_report, :puppet_format, :with_failure) }
|
13
|
+
|
14
|
+
let(:puppet_format) { puppet_report_with_nochange.format }
|
15
|
+
let(:ansible_format) { ansible_report_with_change.format }
|
16
|
+
|
17
|
+
let(:host1) { ansible_report_with_change.host }
|
18
|
+
let(:host2) { puppet_report_with_nochange.host }
|
19
|
+
let(:host3) { puppet_report_with_failure.host }
|
20
|
+
let(:hosts) { [host1, host2, host3] }
|
21
|
+
|
22
|
+
test 'check number of host reports with changes and ansible format' do
|
23
|
+
changed = ansible_report_with_change.change + ansible_report_with_nochange.change + ansible_report_with_failure.change
|
24
|
+
assert_equal change_hosts(ansible_format), changed
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'check number of host reports with changes and puppet format' do
|
28
|
+
changed = puppet_report_with_change.change + puppet_report_with_nochange.change + puppet_report_with_failure.change
|
29
|
+
assert_equal change_hosts(puppet_format), changed
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'check number of host reports with no changes and ansible format' do
|
33
|
+
nochange = ansible_report_with_change.nochange + ansible_report_with_nochange.nochange + ansible_report_with_failure.nochange
|
34
|
+
assert_equal nochange_hosts(ansible_format), nochange
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'check number of host reports with no changes and puppet format' do
|
38
|
+
nochange = puppet_report_with_change.nochange + puppet_report_with_nochange.nochange + puppet_report_with_failure.nochange
|
39
|
+
assert_equal nochange_hosts(puppet_format), nochange
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'check number of host reports with failures and ansible format' do
|
43
|
+
failure = ansible_report_with_change.failure + ansible_report_with_nochange.failure + ansible_report_with_failure.failure
|
44
|
+
assert_equal failure_hosts(ansible_format), failure
|
45
|
+
end
|
46
|
+
|
47
|
+
test 'check number of host reports with failures and puppet format' do
|
48
|
+
failure = puppet_report_with_change.failure + puppet_report_with_nochange.failure + puppet_report_with_failure.failure
|
49
|
+
assert_equal failure_hosts(puppet_format), failure
|
50
|
+
end
|
51
|
+
|
52
|
+
test 'check Hosts with disabled alerts ' do
|
53
|
+
host1.update(:enabled => false)
|
54
|
+
disabled = hosts.reject(&:enabled?)
|
55
|
+
assert_equal disabled_hosts, disabled.count
|
56
|
+
end
|
57
|
+
|
58
|
+
test 'latest_reports' do
|
59
|
+
FactoryBot.create(:host_report, :ansible_format, :with_change)
|
60
|
+
FactoryBot.create(:host_report, :ansible_format, :with_nochange)
|
61
|
+
as_admin do
|
62
|
+
assert_equal latest_reports.count, 2
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/webpack/fills.js
CHANGED
@@ -9,6 +9,12 @@ const fills = [
|
|
9
9
|
component: props => <ReportsTab {...props} />,
|
10
10
|
weight: 450,
|
11
11
|
},
|
12
|
+
{
|
13
|
+
slot: '[puppet]-reports',
|
14
|
+
name: 'Reports',
|
15
|
+
component: props => <ReportsTab format="puppet" {...props} />,
|
16
|
+
weight: 450,
|
17
|
+
},
|
12
18
|
];
|
13
19
|
|
14
20
|
export const registerFills = () => {
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core';
|
4
|
+
import {
|
5
|
+
ExclamationCircleIcon,
|
6
|
+
SyncAltIcon,
|
7
|
+
CheckCircleIcon,
|
8
|
+
} from '@patternfly/react-icons';
|
9
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
10
|
+
|
11
|
+
const StatusToggleGroup = ({ setSelected, selected }) => {
|
12
|
+
const onChange = (isSelected, { currentTarget: { id } }) =>
|
13
|
+
setSelected(prev => ({ ...prev, [id]: isSelected }));
|
14
|
+
|
15
|
+
return (
|
16
|
+
<ToggleGroup aria-label="Icon variant toggle group">
|
17
|
+
<ToggleGroupItem
|
18
|
+
icon={
|
19
|
+
<ExclamationCircleIcon color="var(--pf-global--palette--red-100)" />
|
20
|
+
}
|
21
|
+
text={__('Failed')}
|
22
|
+
aria-label="filter failed icon button"
|
23
|
+
buttonId="failed"
|
24
|
+
isSelected={selected.failed}
|
25
|
+
onChange={onChange}
|
26
|
+
/>
|
27
|
+
<ToggleGroupItem
|
28
|
+
icon={<SyncAltIcon color="var(--pf-global--palette--orange-300)" />}
|
29
|
+
text={__('Changed')}
|
30
|
+
aria-label="filter changed icon button"
|
31
|
+
buttonId="changed"
|
32
|
+
isSelected={selected.changed}
|
33
|
+
onChange={onChange}
|
34
|
+
/>
|
35
|
+
<ToggleGroupItem
|
36
|
+
icon={<CheckCircleIcon color="var(--pf-global--success-color--100)" />}
|
37
|
+
text={__('Unchanged')}
|
38
|
+
aria-label="filter unchanged icon button"
|
39
|
+
buttonId="unchanged"
|
40
|
+
isSelected={selected.unchanged}
|
41
|
+
onChange={onChange}
|
42
|
+
/>
|
43
|
+
</ToggleGroup>
|
44
|
+
);
|
45
|
+
};
|
46
|
+
|
47
|
+
StatusToggleGroup.propTypes = {
|
48
|
+
setSelected: PropTypes.func.isRequired,
|
49
|
+
selected: PropTypes.object,
|
50
|
+
};
|
51
|
+
|
52
|
+
StatusToggleGroup.defaultProps = {
|
53
|
+
selected: { failed: false, changed: false, unchanged: false },
|
54
|
+
};
|
55
|
+
|
56
|
+
export default StatusToggleGroup;
|
@@ -53,7 +53,7 @@ export const statusSummaryFormatter = ({ change, nochange, failure }) => {
|
|
53
53
|
export const globalStatusFormatter = ({ status }) => {
|
54
54
|
switch (status) {
|
55
55
|
case 'failure':
|
56
|
-
return <FailedIcon label={__('
|
56
|
+
return <FailedIcon label={__('Failed')} />;
|
57
57
|
case 'change':
|
58
58
|
return <ChangedIcon label={__('Changed')} />;
|
59
59
|
case 'nochange':
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/* eslint-disable camelcase */
|
2
|
-
import React, { useEffect, useCallback } from 'react';
|
2
|
+
import React, { useEffect, useCallback, useState } from 'react';
|
3
3
|
import { useDispatch, useSelector } from 'react-redux';
|
4
4
|
import { useHistory } from 'react-router-dom';
|
5
5
|
import PropTypes from 'prop-types';
|
@@ -7,7 +7,7 @@ import URI from 'urijs';
|
|
7
7
|
import SearchBar from 'foremanReact/components/SearchBar';
|
8
8
|
import Pagination from 'foremanReact/components/Pagination';
|
9
9
|
import { get } from 'foremanReact/redux/API';
|
10
|
-
import { Grid, GridItem } from '@patternfly/react-core';
|
10
|
+
import { Divider, Grid, GridItem } from '@patternfly/react-core';
|
11
11
|
import {
|
12
12
|
selectAPIStatus,
|
13
13
|
selectAPIResponse,
|
@@ -15,8 +15,9 @@ import {
|
|
15
15
|
import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
|
16
16
|
import { HOST_REPORTS_SEARCH_PROPS } from '../../Router/HostReports/IndexPage/constants';
|
17
17
|
import ReportsTable from './ReportsTable';
|
18
|
+
import StatusToggleGroup from './StatusToggleGroup';
|
18
19
|
|
19
|
-
const ReportsTab = ({ hostName }) => {
|
20
|
+
const ReportsTab = ({ hostName, format }) => {
|
20
21
|
const dispatch = useDispatch();
|
21
22
|
const history = useHistory();
|
22
23
|
const API_KEY = `get-reports-${hostName}`;
|
@@ -25,6 +26,11 @@ const ReportsTab = ({ hostName }) => {
|
|
25
26
|
);
|
26
27
|
const { perPage: settingsPerPage = 20 } = useForemanSettings() || {};
|
27
28
|
const status = useSelector(state => selectAPIStatus(state, API_KEY));
|
29
|
+
const [filters, setFilters] = useState({
|
30
|
+
failed: false,
|
31
|
+
changed: false,
|
32
|
+
unchanged: false,
|
33
|
+
});
|
28
34
|
const fetchReports = useCallback(
|
29
35
|
({ search: searchParam, per_page: perPageParam, page: pageParam } = {}) => {
|
30
36
|
const {
|
@@ -42,13 +48,13 @@ const ReportsTab = ({ hostName }) => {
|
|
42
48
|
params: {
|
43
49
|
page,
|
44
50
|
per_page,
|
45
|
-
search: getServerQuery(search),
|
51
|
+
search: getServerQuery(search, filters),
|
46
52
|
},
|
47
53
|
})
|
48
54
|
);
|
49
55
|
updateUrl({ page, per_page, search });
|
50
56
|
},
|
51
|
-
[API_KEY, dispatch, getServerQuery, getUrlParams, updateUrl]
|
57
|
+
[API_KEY, dispatch, getServerQuery, getUrlParams, updateUrl, filters]
|
52
58
|
);
|
53
59
|
|
54
60
|
useEffect(() => {
|
@@ -61,14 +67,24 @@ const ReportsTab = ({ hostName }) => {
|
|
61
67
|
};
|
62
68
|
|
63
69
|
const getServerQuery = useCallback(
|
64
|
-
search => {
|
70
|
+
(search, _filters) => {
|
65
71
|
const serverQuery = [`host = ${hostName}`];
|
72
|
+
if (format) {
|
73
|
+
serverQuery.push(`format = ${format}`);
|
74
|
+
}
|
66
75
|
if (search) {
|
67
|
-
serverQuery.push(`
|
76
|
+
serverQuery.push(`(${search})`);
|
68
77
|
}
|
69
|
-
|
78
|
+
|
79
|
+
Object.keys(_filters).forEach(filter => {
|
80
|
+
if (_filters[filter]) {
|
81
|
+
serverQuery.push(`${filter} > 0`);
|
82
|
+
}
|
83
|
+
});
|
84
|
+
|
85
|
+
return serverQuery.join(' AND ');
|
70
86
|
},
|
71
|
-
[hostName]
|
87
|
+
[format, hostName]
|
72
88
|
);
|
73
89
|
|
74
90
|
const getUrlParams = useCallback(() => {
|
@@ -94,13 +110,17 @@ const ReportsTab = ({ hostName }) => {
|
|
94
110
|
|
95
111
|
return (
|
96
112
|
<Grid id="new_host_details_insights_tab" hasGutter>
|
97
|
-
<GridItem span={
|
113
|
+
<GridItem span={5}>
|
98
114
|
<SearchBar
|
99
115
|
data={HOST_REPORTS_SEARCH_PROPS}
|
100
116
|
onSearch={search => fetchReports({ search, page: 1 })}
|
101
117
|
/>
|
102
118
|
</GridItem>
|
103
|
-
<GridItem span={
|
119
|
+
<GridItem span={4}>
|
120
|
+
<StatusToggleGroup setSelected={setFilters} selected={filters} />
|
121
|
+
<Divider isVertical />
|
122
|
+
</GridItem>
|
123
|
+
<GridItem span={3}>
|
104
124
|
<Pagination
|
105
125
|
variant="top"
|
106
126
|
itemCount={itemCount}
|
@@ -127,6 +147,11 @@ const ReportsTab = ({ hostName }) => {
|
|
127
147
|
|
128
148
|
ReportsTab.propTypes = {
|
129
149
|
hostName: PropTypes.string.isRequired,
|
150
|
+
format: PropTypes.string,
|
151
|
+
};
|
152
|
+
|
153
|
+
ReportsTab.defaultProps = {
|
154
|
+
format: null,
|
130
155
|
};
|
131
156
|
|
132
157
|
export default ReportsTab;
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_host_reports
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lukas Zapletal
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Fast and efficient reporting capabilities
|
14
14
|
email:
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- app/controllers/concerns/foreman_host_reports/controller/parameters/host_report.rb
|
25
25
|
- app/controllers/host_reports_controller.rb
|
26
26
|
- app/helpers/concerns/foreman_host_reports/hosts_helper_extensions.rb
|
27
|
+
- app/helpers/foreman_host_reports/reports_dashboard_helper.rb
|
27
28
|
- app/models/concerns/foreman_host_reports/host_extensions.rb
|
28
29
|
- app/models/concerns/foreman_host_reports/smart_proxy_extensions.rb
|
29
30
|
- app/models/host_report.rb
|
@@ -34,6 +35,10 @@ files:
|
|
34
35
|
- app/views/api/v2/host_reports/index.json.rabl
|
35
36
|
- app/views/api/v2/host_reports/main.json.rabl
|
36
37
|
- app/views/api/v2/host_reports/show.json.rabl
|
38
|
+
- app/views/dashboard/_host_reports_status_chart_widget.html.erb
|
39
|
+
- app/views/dashboard/_host_reports_status_links.html.erb
|
40
|
+
- app/views/dashboard/_host_reports_status_widget.html.erb
|
41
|
+
- app/views/dashboard/_host_reports_widget.html.erb
|
37
42
|
- config/routes.rb
|
38
43
|
- db/migrate/20210112183526_add_host_reports.rb
|
39
44
|
- db/migrate/20210616133601_create_report_keywords.rb
|
@@ -44,6 +49,7 @@ files:
|
|
44
49
|
- lib/foreman_host_reports/engine.rb
|
45
50
|
- lib/foreman_host_reports/version.rb
|
46
51
|
- lib/tasks/foreman_host_reports_tasks.rake
|
52
|
+
- lib/tasks/migrate.rake
|
47
53
|
- locale/Makefile
|
48
54
|
- locale/en/foreman_host_reports.po
|
49
55
|
- locale/foreman_host_reports.pot
|
@@ -54,6 +60,7 @@ files:
|
|
54
60
|
- test/model/host_report_status_test.rb
|
55
61
|
- test/snapshots/foreman-web.json
|
56
62
|
- test/test_plugin_helper.rb
|
63
|
+
- test/unit/dashboard_test.rb
|
57
64
|
- test/unit/foreman_host_reports_test.rb
|
58
65
|
- webpack/__mocks__/foremanReact/common/HOC.js
|
59
66
|
- webpack/__mocks__/foremanReact/common/I18n.js
|
@@ -120,6 +127,7 @@ files:
|
|
120
127
|
- webpack/src/Router/HostReports/constants.js
|
121
128
|
- webpack/src/Router/routes.js
|
122
129
|
- webpack/src/components/ReportsTab/ReportsTable.js
|
130
|
+
- webpack/src/components/ReportsTab/StatusToggleGroup.js
|
123
131
|
- webpack/src/components/ReportsTab/helpers.js
|
124
132
|
- webpack/src/components/ReportsTab/index.js
|
125
133
|
homepage: https://github.com/theforeman/foreman_host_reports
|
@@ -142,13 +150,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
150
|
- !ruby/object:Gem::Version
|
143
151
|
version: '0'
|
144
152
|
requirements: []
|
145
|
-
rubygems_version: 3.1.
|
153
|
+
rubygems_version: 3.1.6
|
146
154
|
signing_key:
|
147
155
|
specification_version: 4
|
148
156
|
summary: Foreman reporting engine
|
149
157
|
test_files:
|
150
158
|
- test/factories/foreman_host_reports_factories.rb
|
151
159
|
- test/unit/foreman_host_reports_test.rb
|
160
|
+
- test/unit/dashboard_test.rb
|
152
161
|
- test/controllers/api/v2/host_reports_controller_test.rb
|
153
162
|
- test/snapshots/foreman-web.json
|
154
163
|
- test/model/host_report_status_test.rb
|