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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9695286b83e23f261081f2cf90295bf1eee04ac44d34008873ab108bb8293bc6
4
- data.tar.gz: 6db378669511e31557bfd2f846b3cbd3dbe20e2bf10c81a1682ee6d8fe469578
3
+ metadata.gz: 0ef176b8d987ed56aa02950203e7cfdfc880d050be4701377c15aa8bcd20559a
4
+ data.tar.gz: 58a23f1ad805649d1dafffa7dcfe2af89c733380476588ae0b31df34b6502c68
5
5
  SHA512:
6
- metadata.gz: 8a504fa405d02d5b2a5f65530e818e1ba41aaa94aefaf1df199aa7cc0c494c4fe83f49702951a2f253ca442c761e99e109875b6d3aa8ba743cd622b60d8d7b88
7
- data.tar.gz: de05ed9c8babe9e6b687db9db1a5052b621cee8dd1757e6013fc2f196181bed4ffed070b7bee2cfd727f49e5ceeda8372aa7a01b6f411d5891269652e8e58649
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
 
@@ -1,3 +1,3 @@
1
1
  module ForemanHostReports
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.0.2'.freeze
3
3
  end
@@ -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": "^8.15.0"
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
- "@sheerun/mutationobserver-shim": "^0.3.3",
32
- "@theforeman/builder": "^8.15.0",
33
- "@theforeman/eslint-plugin-foreman": "^8.15.0",
34
- "@theforeman/find-foreman": "^8.15.0",
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={__('Fail')} />;
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(`AND (${search})`);
76
+ serverQuery.push(`(${search})`);
68
77
  }
69
- return serverQuery.join(' ').trim();
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={6}>
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={6}>
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.1
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-02-22 00:00:00.000000000 Z
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.4
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