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