foreman_host_reports 0.0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -447
  3. data/app/controllers/api/v2/host_reports_controller.rb +39 -20
  4. data/app/controllers/concerns/foreman_host_reports/controller/hosts_controller_extensions.rb +18 -0
  5. data/app/controllers/concerns/foreman_host_reports/controller/parameters/host_report.rb +1 -1
  6. data/app/controllers/host_reports_controller.rb +32 -2
  7. data/app/helpers/concerns/foreman_host_reports/hosts_helper_extensions.rb +25 -0
  8. data/app/models/concerns/foreman_host_reports/host_extensions.rb +6 -0
  9. data/app/models/host_report.rb +15 -0
  10. data/app/models/host_status/host_report_status.rb +185 -0
  11. data/app/views/api/v2/host_reports/main.json.rabl +1 -2
  12. data/config/routes.rb +2 -4
  13. data/db/migrate/20220113064436_rename_status_summaries.rb +12 -0
  14. data/lib/foreman_host_reports/engine.rb +11 -5
  15. data/lib/foreman_host_reports/version.rb +1 -1
  16. data/test/controllers/api/v2/host_reports_controller_test.rb +30 -67
  17. data/test/factories/foreman_host_reports_factories.rb +13 -5
  18. data/test/model/host_report_status_test.rb +204 -0
  19. data/test/test_plugin_helper.rb +4 -2
  20. data/webpack/__mocks__/foremanReact/components/Pagination/index.js +2 -0
  21. data/webpack/fills.js +23 -0
  22. data/webpack/global_index.js +2 -0
  23. data/webpack/src/Router/HostReports/IndexPage/Components/HostReportsTable/Components/Formatters/statusFormatter.js +3 -4
  24. data/webpack/src/Router/HostReports/IndexPage/Components/HostReportsTable/Components/StatusCell.js +1 -1
  25. data/webpack/src/Router/HostReports/IndexPage/Components/HostReportsTable/HostReportsTable.js +2 -10
  26. data/webpack/src/Router/HostReports/IndexPage/IndexPage.js +0 -1
  27. data/webpack/src/Router/HostReports/IndexPage/IndexPageActions.js +5 -4
  28. data/webpack/src/Router/HostReports/IndexPage/IndexPageHelpers.js +1 -1
  29. data/webpack/src/Router/HostReports/IndexPage/__tests__/HostReportsIndexPage.test.js +3 -4
  30. data/webpack/src/Router/HostReports/IndexPage/__tests__/__snapshots__/HostReportsIndexPage.test.js.snap +4 -6
  31. data/webpack/src/Router/HostReports/IndexPage/constants.js +4 -3
  32. data/webpack/src/Router/HostReports/ShowPage/Components/ReportLogs/Ansible.js +62 -27
  33. data/webpack/src/Router/HostReports/ShowPage/Components/ReportLogs/Components/EmptyLogsRow.js +27 -0
  34. data/webpack/src/Router/HostReports/ShowPage/Components/ReportLogs/Components/RawMsgModal.js +41 -0
  35. data/webpack/src/Router/HostReports/ShowPage/Components/ReportLogs/Puppet.js +38 -37
  36. data/webpack/src/Router/HostReports/ShowPage/Components/ReportLogs/index.js +10 -3
  37. data/webpack/src/Router/HostReports/ShowPage/Components/ReportLogsFilter/index.js +56 -65
  38. data/webpack/src/Router/HostReports/ShowPage/ShowPage.js +34 -8
  39. data/webpack/src/Router/HostReports/constants.js +2 -0
  40. data/webpack/src/components/ReportsTab/ReportsTable.js +117 -0
  41. data/webpack/src/components/ReportsTab/helpers.js +155 -0
  42. data/webpack/src/components/ReportsTab/index.js +132 -0
  43. metadata +18 -19
  44. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 488222c4e90b62231ba01365932c00ccb2619ea7f8225757bf84c423cd56b13a
4
- data.tar.gz: 39f11ab9a01e780bff2105f09726f95b564d45169b6c9c2466e9846f0433aff8
3
+ metadata.gz: 9bc62a12f8d2fe8355e22c7559102e007458392595d50779a817e6d1cdef70b1
4
+ data.tar.gz: be76cd5054934fd0f32c1b76c0348927550ab603c43d5f7c6b272d8588960311
5
5
  SHA512:
6
- metadata.gz: 9970feb6d0dd2593977eded7cade385a1a17743da6af330f3c5fb1c338dd62a63716b07b19bc5fd7fef1b22bd20147d08d9fc41639f570c6c2b43cd02100a36d
7
- data.tar.gz: 76f80c8de18583d372beee125c791f8eef7fd41bbcb501c318951ab7708a311eb6905f3e94b5df841700c9c2f15a1157985cb23d8ea5661aac7f45d738286879
6
+ metadata.gz: 4f7582923d7c31f3fa6a8659f6cb8dbb3f703129943c2fb98943e1be1eac47c8f45ff1a52c48d15b8f92004b9827c5682ddf95edaef9101a9752f32b3eb1ad26
7
+ data.tar.gz: 135a20d6085fd644f11606c85f4b9a0c754e491afb5ce8314aabfadb45f5370ac0f876b0c6ab2c22a0b60d7631fcc0f5276a978c18212dea095177fee9ecec8e
data/README.md CHANGED
@@ -11,55 +11,6 @@ for how to install Foreman plugins.
11
11
 
12
12
  More instructions later.
13
13
 
14
- ## Motivation
15
-
16
- The motivation was poor performance of the Foreman core reporting
17
- implementation. If reports are processed and stored efficiently, import process
18
- can be 20x (times) faster with less storage requirements. To achieve that,
19
- processing is offloaded to smart proxy and reports are stripped down to minimum
20
- relevant information during the import phase, report is stored as-is
21
- in a database blob (text) field and only parsed when report show page is
22
- opened.
23
-
24
- For more info and discussion about the implementation, read [a thread on
25
- discourse](https://community.theforeman.org/t/rfc-optimized-reports-storage/15573).
26
- The design is roughly the following:
27
-
28
- * Instead of modyfing current core report functionality which is customized by various plugins, new plugin is created.
29
- * New model class is created: `HostReport`, the reason for that is that this will be better upgrade experience, new tables can be migrated, data can be transformed and legacy tables `Report` and `ConfigReport` can be dropped afterwards. API will be completely different anyway, plugins will no longer handle directly with the model anymore.
30
- * New API endpoint is created on smart proxy instead of foreman, this will require modifications of ENC script, Ansible and OpenSCAP.
31
- * All input processing is done on smart proxy, report is then forwarded to Foreman and directly stored in the database.
32
- * The API remains very same, tho with some differences. Report is sent in JSON, required HTTP arguments "format" and "hostname" are required to allow storing reports without any input parsing (copy directly into database).
33
- * Report content is stored in new field `body` as plain text (JSON) which is compressed by default by Postgres server.
34
- * Foreman Puppet report format is modified for better performance - instead of individual log lines, whole report body is stored in one JSON string called "body". Parsing is done during report view/download from Foreman.
35
- * Report origin is kept in a new `format` field (Foreman Puppet, Ansible, OpenSCAP, Plaintext). There is report format Plaintext which would be simply store report as-is without further processing.
36
- * Status column converted from integer to 64bit big int.
37
- * StatusCalculator is kept and extended to use all 64bits.
38
- * Plugin decides themselves how to store data in the `body` field in such a way that it’s presentable and searchable. Plugin authors should be dis-encouraged from complex (and slow) transformations tho - transformation during view should be encouraged.
39
- * New model `ReportKeyword(id: int, report_id: int, name: varchar)` is created so plugins can create arbitrary number of keywords which are associated with Report model (M:N).
40
- * Example keywords:
41
- * `PuppetHasFailedResource`
42
- * `PuppetHasFailedRestartResource`
43
- * `PuppetHasChangedResource`
44
- * `AnsibleHasUnreachableHost`
45
- * `AnsibleHasFailedTask`
46
- * `AnsibleHasChangedTask`
47
- * `ScapHasFailedRule`
48
- * `ScapHasOtheredRule`
49
- * `ScapHasHighSeverityFailure`
50
- * `ScapHasMediumSeverityFailure`
51
- * `ScapHasLowSeverityFailure`
52
- * `ScapFailure:xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed`
53
- * `ScapFailure:xccdf_org.ssgproject.content_rule_security_patches_up_to_date`
54
- * It is completely up to plugin authors which set of keywords they will generate.
55
- * Keyword generation can be configurable, for example OpenSCAP plugin can have a list of allowed rules to report (the most important ones).
56
- * The key is to keep amount of keywords at a reasonable level, for example OpenSCAP should not be creating `ScapPassed:xyz` keywords because there will be too many of them.
57
- * Searching is supported via:
58
- * Indexed keywords (e.g. `origin = scap and keyword = ScapHasHighSeverityFailure` or simply just the keyword which will be the default scoped_search field)
59
- * Full text in body (slow but this will work for searching for particular line)
60
- * Index page (search result) shows also number of failures, changes etc (using StatusCalculator).
61
- * All searching should be by default scoped to a reasonable time frame (last week) so SQL server can quickly use index on “reported_at” column and do a quick table scan for the rest
62
-
63
14
  ## Report types
64
15
 
65
16
  There are several report types implemented by this plugin.
@@ -67,38 +18,25 @@ There are several report types implemented by this plugin.
67
18
  The API REST endpoint expects the format to be also passed via HTTP argument so
68
19
  no input parsing is actually needed to detect format.
69
20
 
70
- ### Plain
21
+ ### Plain text
71
22
 
72
- The most simple format, whole contents of HTTP payload is stored in body
73
- database field without any processing. When such format is displayed, it is
74
- presented as plain/text without any transformations or formatting.
75
-
76
- For backward compatibility, the plugin will override the core reports API and
77
- store incoming reports as plaintext so nothing gets lost, reports will be still
78
- visible (but formatting will be human-unreadable) so users can take actions to
79
- reconfigure reporting via the new import API.
80
-
81
- ### Standard
82
-
83
- Standard format that is optimized for fast processing and effective storage.
84
- Format, host and reported_at fields are all mandatory.
23
+ Plain format can be used for unknown formats as a fallback or to upload
24
+ arbitrary log file into Foreman.
85
25
 
86
26
  Optional field id should be set to unique number or string that represetnts the
87
27
  report. Optional proxy field represents FQDN of foreman proxy which processed
88
28
  the report.
89
29
 
90
- Optional status field represent counts of levels for the report, this is stored
91
- in 64 bit array. Standard report recognizes the following levels: debug,
92
- normal, warning and error. If count exceeds the limit if unsigned 16 bits per
93
- number, report returns "65536+" for this status.
94
-
95
- Optional field errors may contain an array of strings with errors from initial
96
- processing and transformation.
30
+ Optional summary fields change, nochange and failure are integers which have
31
+ different semantics for each type. For more information about the mapping, read
32
+ [the initial design
33
+ discussion](https://community.theforeman.org/t/new-config-report-summary-columns/26531).
34
+ For plain reports, summary fields are not used tho.
97
35
 
98
- Field named all_lines is a simple string to minimize memory allocations,
99
- usually a multi-line string. The standard implementation does not perform any
36
+ Field named body is a simple string to minimize memory allocations,
37
+ usually a multi-line string. The plain implementation does not perform any
100
38
  formatting or transformations, when implementing a new formatter it is
101
- recommended to keep all_lines field as a multi-line string and prefix lines
39
+ recommended to keep body field as a multi-line string and prefix lines
102
40
  with additional info like level or timestamp:
103
41
 
104
42
  ```
@@ -106,417 +44,99 @@ INFO:1611047686:All log lines are represented as a single multi-line string.
106
44
  DEBUG:1611047687:This is a second line.
107
45
  ```
108
46
 
109
- The standard format is good enough for standard output and error of UNIX
47
+ The plain format is good enough for plain output and error of UNIX
110
48
  terminal or syslog output. If multiple lines are expected (e.g. output of files
111
49
  for diff), JSON array should be considered instead. See Puppet format below for
112
50
  more details.
113
51
 
114
52
  It is recommended to avoid performing transformations during storing of reports
115
53
  into database as reporting must be optimized for fast uploads. All
116
- transformations (e.g. turning all_lines into a HTML table with three columns
54
+ transformations (e.g. turning body into a HTML table with three columns
117
55
  for the example above) should be done when a report is fetched and displayed.
118
56
 
119
- Whole JSON is stored in "body" database field, so additional fields can be
120
- added by implementations based on the standard report. An example standard
121
- report:
57
+ Example:
122
58
 
123
59
  ```
124
60
  {
125
- "format": "standard",
61
+ "format": "plain",
126
62
  "id": "06b77b5d-5df5-4937-9c14-d00a2e7b927f",
127
63
  "host": "hostname.example.com",
128
64
  "proxy": "foreman-proxy.example.com",
129
65
  "reported_at": "2013-05-13 19:49:00 UTC",
130
- "errors": [],
131
- "status": {
132
- "debug": 0,
133
- "normal": 41,
134
- "warning": 1,
135
- "error": 2
136
- },
137
- "all_lines": "All log lines are represented as a single multi-line string.\nThis is a second line."
66
+ "change" : 0,
67
+ "nochange" : 0,
68
+ "failure" : 0,
69
+ "body": "All log lines are represented as a single multi-line string.\nThis is a second line."
138
70
  }
139
71
  ```
140
72
 
73
+ #### Keywords
74
+
75
+ Each report can contain field named keyword, an array of strings. These are
76
+ strings, or tags, stored in a separate table with an index and associated with
77
+ the report. Keyword should be in CamelCase, prefixed with the report type (e.g.
78
+ `Puppet`).
79
+
141
80
  ### Puppet
142
81
 
143
82
  Report designed to fullfill needs of the legacy Foreman Puppet report based on
144
- the standard report. It shares common fields with the standard report (see
145
- above) but it has the following statuses: applied, restarted, failed,
146
- failed_restarts, skipped, pending. That's 10 bits per status with maximum value
147
- of 1024.
83
+ the plain report. A lot of information is passed unchanged from the original
84
+ puppet YAML report, some detailed information is dropped for smaller size tho.
148
85
 
149
- Instead of all_lines, contents is stored in logs array with every log being
150
- array of three elements:
86
+ Contents (log lines) is stored in logs array with every log being array of
87
+ three elements:
151
88
 
152
89
  * level: one of debug, info, notice, warning, err, alert, emerg, crit
153
90
  * source: the puppet resource
154
91
  * message: the message itself
155
92
 
156
- Field resource_statuses only contains list of resources, but not more details.
93
+ Field `resource_statuses` only contains list of resources, but not more details.
157
94
 
158
- Field evaluation_times contains top 30 resource names and its evaluation times
95
+ Field `evaluation_times` contains top 30 resource names and its evaluation times
159
96
  plus total sum under name of "Others" for the rest.
160
97
 
161
- ```
162
- {
163
- "format": "puppet",
164
- "id": "06b77b5d-5df5-4937-9c14-d00a2e7b927f",
165
- "host": "deb.example.com",
166
- "proxy": "localhost",
167
- "reported_at": "2021-01-19T12:40:02.831013816Z",
168
- "report_format": 10,
169
- "puppet_version": "6.16.0",
170
- "environment": "production",
171
- "metrics": {
172
- "resources": {
173
- "name": "resources",
174
- "label": "Resources",
175
- "values": [
176
- [
177
- "total",
178
- "Total",
179
- 405
180
- ],
181
- [
182
- "skipped",
183
- "Skipped",
184
- 0
185
- ],
186
- [
187
- "failed",
188
- "Failed",
189
- 1
190
- ],
191
- [
192
- "failed_to_restart",
193
- "Failed to restart",
194
- 0
195
- ],
196
- [
197
- "restarted",
198
- "Restarted",
199
- 0
200
- ],
201
- [
202
- "changed",
203
- "Changed",
204
- 0
205
- ],
206
- [
207
- "out_of_sync",
208
- "Out of sync",
209
- 0
210
- ],
211
- [
212
- "scheduled",
213
- "Scheduled",
214
- 0
215
- ],
216
- [
217
- "corrective_change",
218
- "Corrective change",
219
- 0
220
- ]
221
- ]
222
- },
223
- "time": {
224
- "name": "time",
225
- "label": "Time",
226
- "values": [
227
- [
228
- "package",
229
- "Package",
230
- 0.10441856899999999
231
- ],
232
- [
233
- "file",
234
- "File",
235
- 0.6205165560000004
236
- ],
237
- [
238
- "anchor",
239
- "Anchor",
240
- 0.0007514900000000001
241
- ],
242
- [
243
- "exec",
244
- "Exec",
245
- 2.5235192609999997
246
- ],
247
- [
248
- "file_line",
249
- "File line",
250
- 0.000395901
251
- ],
252
- [
253
- "mysql_datadir",
254
- "Mysql datadir",
255
- 0.000380248
256
- ],
257
- [
258
- "service",
259
- "Service",
260
- 0.187978321
261
- ],
262
- [
263
- "mysql_user",
264
- "Mysql user",
265
- 0.000324085
266
- ],
267
- [
268
- "mysql_grant",
269
- "Mysql grant",
270
- 0.0005333
271
- ],
272
- [
273
- "group",
274
- "Group",
275
- 0.0016234639999999998
276
- ],
277
- [
278
- "shellvar",
279
- "Shellvar",
280
- 0.095874028
281
- ],
282
- [
283
- "mounttab",
284
- "Mounttab",
285
- 0.008913022
286
- ],
287
- [
288
- "user",
289
- "User",
290
- 0.014042565000000002
291
- ],
292
- [
293
- "ssh_authorized_key",
294
- "Ssh authorized key",
295
- 0.000289458
296
- ],
297
- [
298
- "augeas",
299
- "Augeas",
300
- 0.075545717
301
- ],
302
- [
303
- "gnupg_key",
304
- "Gnupg key",
305
- 0.021904382
306
- ],
307
- [
308
- "mailalias",
309
- "Mailalias",
310
- 0.000313646
311
- ],
312
- [
313
- "sshd_config",
314
- "Sshd config",
315
- 0.034943459
316
- ],
317
- [
318
- "postgresql_conf",
319
- "Postgresql conf",
320
- 0.001839983
321
- ],
322
- [
323
- "concat_file",
324
- "Concat file",
325
- 0.000588799
326
- ],
327
- [
328
- "concat_fragment",
329
- "Concat fragment",
330
- 0.0027495480000000005
331
- ],
332
- [
333
- "rvm_system_ruby",
334
- "Rvm system ruby",
335
- 1.384307727
336
- ],
337
- [
338
- "rvm_alias",
339
- "Rvm alias",
340
- 1.48978911
341
- ],
342
- [
343
- "postgresql_conn_validator",
344
- "Postgresql conn validator",
345
- 0.066834735
346
- ],
347
- [
348
- "postgresql_psql",
349
- "Postgresql psql",
350
- 0.549115666
351
- ],
352
- [
353
- "cron",
354
- "Cron",
355
- 0.006601505
356
- ],
357
- [
358
- "filebucket",
359
- "Filebucket",
360
- 0.000183308
361
- ],
362
- [
363
- "startup_time",
364
- "Startup time",
365
- 0.733938909
366
- ],
367
- [
368
- "node_retrieval",
369
- "Node retrieval",
370
- 1.081311710178852
371
- ],
372
- [
373
- "plugin_sync",
374
- "Plugin sync",
375
- 1.6488488875329494
376
- ],
377
- [
378
- "fact_generation",
379
- "Fact generation",
380
- 3.781282566487789
381
- ],
382
- [
383
- "convert_catalog",
384
- "Convert catalog",
385
- 0.9406693913042545
386
- ],
387
- [
388
- "config_retrieval",
389
- "Config retrieval",
390
- 6.882053259760141
391
- ],
392
- [
393
- "transaction_evaluation",
394
- "Transaction evaluation",
395
- 7.9571788385510445
396
- ],
397
- [
398
- "catalog_application",
399
- "Catalog application",
400
- 8.027862664312124
401
- ],
402
- [
403
- "total",
404
- "Total",
405
- 23.256689724
406
- ]
407
- ]
408
- },
409
- "changes": {
410
- "name": "changes",
411
- "label": "Changes",
412
- "values": [
413
- [
414
- "total",
415
- "Total",
416
- 0
417
- ]
418
- ]
419
- },
420
- "events": {
421
- "name": "events",
422
- "label": "Events",
423
- "values": [
424
- [
425
- "total",
426
- "Total",
427
- 0
428
- ],
429
- [
430
- "failure",
431
- "Failure",
432
- 0
433
- ],
434
- [
435
- "success",
436
- "Success",
437
- 0
438
- ]
439
- ]
440
- }
441
- },
442
- "logs": [
443
- [
444
- "notice",
445
- "//deb.example.com/Puppet",
446
- "Applied catalog in 8.03 seconds"
447
- ]
448
- ],
449
- "resource_statuses": [
450
- "Package[git]",
451
- "Package[libxml2-dev]",
452
- "Package[libxslt1-dev]",
453
- "Package[libkrb5-dev]",
454
- "Package[libsystemd-dev]",
455
- "Package[freeipmi]",
456
- "Package[ipmitool]",
457
- "Package[firefox-esr]",
458
- "Package[libvirt-dev]",
459
- "Package[asciidoc]",
460
- "Package[bzip2]",
461
- "Package[unzip]",
462
- "Package[ansible]",
463
- "Package[python-virtualenv]",
464
- "Package[libcurl4-openssl-dev]",
465
- "Package[libsqlite3-dev]",
466
- "Package[transifex-client]",
467
- "Package[java]",
468
- "Exec[update-java-alternatives]",
469
- "File_line[java-home-environment]",
470
- "Cron[puppet]",
471
- "Filebucket[puppet]"
472
- ],
473
- "keywords": [
474
- "PuppetResourceFailed:Package[bzip2]"
475
- ],
476
- "evaluation_times": [
477
- [
478
- "Exec[ruby-2.6.3/update_rubygems]",
479
- 0.662541335
480
- ],
481
- [
482
- "Exec[ruby-2.5.1/update_rubygems]",
483
- 0.607295328
484
- ],
485
- [
486
- "Exec[ruby-2.7.0/update_rubygems]",
487
- 0.600359435
488
- ],
489
- [
490
- "Others",
491
- 0.447703594
492
- ]
493
- ]
494
- }
495
- ```
98
+ #### Examples
496
99
 
497
- ## Keywords
100
+ To see examples of puppet reports, visit [snapshot directory](test/snapshots).
498
101
 
499
- Each report can contain field named keyword, an array of strings. These are
500
- strings, or tags, stored in a separate table with an index and associated with
501
- the report. Keyword should be in CamelCase, prefixed with the report type (e.g.
502
- `Puppet`). Reports must avoid creating too many keywords! Typically only
503
- failures should be reported.
102
+ #### Keywords
103
+
104
+ For more info read [keywords mapping initial discussion thread]
105
+ (https://community.theforeman.org/t/rfc-optimized-reports-storage/15573).
504
106
 
505
107
  Example keywords:
506
108
 
507
- * PuppetHasChange
508
- * PuppetIsOutOfSync
509
- * PuppetHasFailure
510
- * PuppetResourceFailed:Package[git]
109
+ * PuppetStatusChanged
110
+ * PuppetNoop
111
+ * PuppetOutOfSync
112
+
113
+ ### Ansible
114
+
115
+ Ansible report also shares common fields with the plain report (format,
116
+ version, host, reported_at, proxy). The body contains JSON representation of
117
+ Ansible report without any changes.
118
+
119
+ Values which were reported as `None` (`nil` in Python) are filtered off tho to
120
+ keep the report size small as Ansible tend to report many of these.
121
+
122
+ #### Examples
511
123
 
512
- ## Other ideas
124
+ To see examples of puppet reports, visit [snapshot directory](test/snapshots).
513
125
 
514
- Reports can have "archived" flag and during import into Foreman, reports could
515
- be stored into a directories alongside database for archival purposes. Deleting
516
- old reports was always huge pain in Foreman, but in this scenario it would be
517
- as easy as "set archived flag and delete body for all records older than X
518
- days". Those reports could remain in the database for longer period of time
519
- without any body, they could be side-loaded from the directory when needed.
126
+ #### Keywords
127
+
128
+ For more info read [keywords mapping initial discussion thread]
129
+ (https://community.theforeman.org/t/rfc-optimized-reports-storage/15573).
130
+
131
+ Example keywords:
132
+
133
+ * AnsibleChanged
134
+ * AnsibleRescued
135
+
136
+ ## Motivation and initial design
137
+
138
+ For more info and discussion about the implementation, read [a thread on
139
+ discourse](https://community.theforeman.org/t/rfc-optimized-reports-storage/15573).
520
140
 
521
141
  ## Contributing
522
142