foreman_host_reports 1.0.1 → 1.0.2
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 +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
|