ruby-grafana-reporter 0.2.2 → 0.3.0

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: 0aff73969c807020f8c39fe5a9c4095a3a8ee093e5fe94982a12f0899c057d44
4
- data.tar.gz: 179f0d922e2a2a477c04cb91d4fffc692c99fdf394f78fef8ff6bfed0a0b5817
3
+ metadata.gz: '00728deba4250ed327170393bbeaac1b1d6d1002463a5a243285ac570f09231f'
4
+ data.tar.gz: 2985a11f6404e622ff279dbe3e328b38e4202bf665b2fef9e23746bbfd6c3459
5
5
  SHA512:
6
- metadata.gz: e2d4730cbac1a089b9bb23eff305f5e6f9b189c66129370ab16400ec0edf993b8f5350305eccce29d4ffb1472d3e80d548be640ef4ac529babb14b3a8066e957
7
- data.tar.gz: 74dfd8961382f3a9e0e35db3557a46762df27843f67cb17be6d0a3b530ff05de4ea09384a4beebfc40716f07b357ffa6eab1fbe0868c63140eeb7de6e2d3c5a1
6
+ metadata.gz: 92de552918a451f9bac1196dfbb05dc1ad64e0059f4c90fe06c079e5e181fee96a07858ae0c5815e3dbf37358c4aa271fcd8d129bb6721f3c8a53f285325fc25
7
+ data.tar.gz: aff95df6e4b5a8636905ec498433d1fcdae6d7b9aa2d26640815a68160c2f74c39078adfbe32a67c9115855ddd97683c6844ea0347e18cf010586e2bd036faed
data/README.md CHANGED
@@ -12,6 +12,7 @@ Reporting Service for Grafana
12
12
  * [Getting started](#getting-started)
13
13
  * [Grafana integration](#grafana-integration)
14
14
  * [Webservice overview](#webservice-overview)
15
+ * [Documentation](#documentation)
15
16
  * [Features](#features)
16
17
  * [Roadmap](#roadmap)
17
18
  * [Contributing](#contributing)
@@ -38,28 +39,28 @@ integrate without further dependencies with the asciidoctor docker image.
38
39
  Can't wait to see, what functions the reporter provides within the asciidoctor
39
40
  templates? Have a look at the [function documentation](FUNCTION_CALLS.md).
40
41
 
41
- The complete
42
- [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) can be
43
- found here.
44
-
45
42
  ## Getting started
46
43
 
47
- There exist several ways of installing the reporter. Here I cover the easiest
48
- setup by using ruby gems. If you need further installation help, or want to use
49
- a "baremetal" ruby setup or a docker integration, please have a look at the more
50
- extended [installation documentation](INSTALL.md).
44
+ There exist several ways of installing the reporter. If you need further
45
+ installation help, or want to use a "baremetal" ruby setup or a docker
46
+ integration, please have a look at the more extended
47
+ [installation documentation](INSTALL.md).
51
48
 
52
- To install the reporter as a gem, simply run:
49
+ Windows users may directly use the provided executable.
53
50
 
51
+ Following these steps sets up the reporter on a fresh Raspberry Pi installation:
52
+
53
+ sudo apt-get install ruby
54
54
  gem install ruby-grafana-reporter
55
55
 
56
- If no configuration file is in place, you might want to use the configuration
57
- wizard, which leads you through all necessary steps:
56
+ That's it. Let's now configure a grafana setup with the configuration wizard:
58
57
 
59
58
  ruby-grafana-reporter -w
60
59
 
61
60
  It is strongly recommended, to also create the demo PDF file, as stated at the end
62
61
  of the procedure, to get a detailed documentation of all the reporter capabilities.
62
+ The whole [function documentation](FUNCTION_CALLS.md) is also available at the
63
+ previous link.
63
64
 
64
65
  To run the reporter as a service, you only need to call it like this:
65
66
 
@@ -112,39 +113,43 @@ Running the reporter as a webservice provides the following URLs
112
113
  /view_report - for viewing the status or receving the result of a specific rendering, is automatically called after a successfull /render call
113
114
  /cancel_report - for cancelling the rendering of a specific report, normally not called manually, but on user interaction in the /view_report or /overview URL
114
115
 
116
+ The main endpoint to call for report generation is configured in the previous chapter [Grafana integration](#grafana-integration).
117
+
118
+ However, if you would like to see, currently running report generations and previously generated reports, you may want to call the endpoint `/overview`.
119
+
120
+ ## Documentation
121
+
122
+ The [function documentation](FUNCTION_CALLS.md) contains a complete overview of
123
+ all possible grafana calls, to generate dynamic report templates.
124
+
125
+ The [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) can be
126
+ found here.
127
+
115
128
  ## Features
116
129
 
117
130
  * Build report template including all imaginable grafana content:
118
131
  * panels as images
119
132
  * panel table query or custom query results as real document tables (not images!)
120
133
  * single panel value or custom query single value result integrated in texts
121
- * Solid as a rock, also in case of template errors (at least it aims to be)
122
- * Runs standalone or as a webservice
134
+ * Solid as a rock, also in case of template errors and whatever else may happen
135
+ * Fully controllable as command line application or as a webservice
123
136
  * Seamlessly integrates with asciidoctor docker container
124
- * Developed for being able to support other tools than asciidoctor as well
137
+ * Developed to support other tools than asciidoctor as well
125
138
 
126
139
  ## Roadmap
127
140
 
128
141
  This is just a collection of things, I am heading for in future, without a schedule.
129
142
 
130
- * Add documentation of possible asciidoctor calls to grafana
131
143
  * Add a simple plugin system to support specific asciidoctor modifications
132
144
  * Solve code TODOs
133
145
  * Become [rubocop](https://rubocop.org/) ready
146
+ * Clean and properly setup test cases
134
147
 
135
148
  ## Contributing
136
149
 
137
150
  If you'd like to contribute, please fork the repository and use a feature
138
151
  branch. Pull requests are warmly welcome.
139
152
 
140
- Though not yet valid for my code, I'd like to see the project become
141
- [rubocop](https://rubocop.org/) ready :-)
142
-
143
- Definitely open spots from my side are:
144
-
145
- * This README
146
- * Clean and properly setup test cases
147
-
148
153
  ## Licensing
149
154
 
150
155
  The code in this project is licensed under MIT license.
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative '../lib/ruby-grafana-reporter'
5
- GrafanaReporter::Application::Application.new.configure_and_run(ARGV)
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/ruby-grafana-reporter'
5
+ GrafanaReporter::Application::Application.new.configure_and_run(ARGV) unless defined?(Ocra)
data/lib/VERSION.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Version information
4
- GRAFANA_REPORTER_VERSION = [0, 2, 2].freeze
5
- GRAFANA_REPORTER_RELEASE_DATE = '2021-01-30'
4
+ GRAFANA_REPORTER_VERSION = [0, 3, 0].freeze
5
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-03-02'
@@ -13,16 +13,15 @@ module Grafana
13
13
  # trailing slash, e.g. +https://localhost:3000+.
14
14
  # @param key [String] API key for the grafana instance, if required
15
15
  # @param opts [Hash] additional options.
16
- # Currently supporting +:logger+ and +:datasources+.
17
- # +:datasources+ need to be an Hash with datasource name as key and datasource id as value.
18
- # If not specified, the datasources will be queried from the grafana interface.
19
- # Specifying +:datasources+> here can be used, so that the interface can be used without grafana Admin privileges.
16
+ # Currently supporting +:logger+ and +:ssl_cert+.
20
17
  def initialize(base_uri, key = nil, opts = {})
21
18
  @base_uri = base_uri
22
19
  @key = key
23
20
  @dashboards = {}
21
+ @ssl_cert = opts[:ssl_cert]
24
22
  @logger = opts[:logger] || ::Logger.new(nil)
25
- @datasources = opts[:datasources]
23
+
24
+ initialize_datasources unless @base_uri.empty?
26
25
  end
27
26
 
28
27
  # Used to test a connection to the grafana instance.
@@ -34,14 +33,13 @@ module Grafana
34
33
  def test_connection
35
34
  if execute_http_request('/api/datasources').is_a?(Net::HTTPOK)
36
35
  # we have admin rights
37
- @logger.info('Reporter is running with Admin privileges on grafana.')
36
+ @logger.warn('Reporter is running with Admin privileges on grafana. This is a potential security risk.')
38
37
  return 'Admin'
39
38
  end
40
39
  # check if we have lower rights
41
40
  return 'Failed' unless execute_http_request('/api/dashboards/home').is_a?(Net::HTTPOK)
42
41
 
43
- @logger.info('Reporter is running with NON-Admin privileges on grafana. Make sure that necessary '\
44
- 'datasources are specified in CONFIG_FILE, otherwise operation will fail')
42
+ @logger.info('Reporter is running with NON-Admin privileges on grafana.')
45
43
  'NON-Admin'
46
44
  end
47
45
 
@@ -49,17 +47,17 @@ module Grafana
49
47
  #
50
48
  # @return [Integer] ID for the specified datasource name
51
49
  def datasource_id(datasource_name)
52
- id = datasources[datasource_name]
53
- raise DatasourceDoesNotExistError.new('name', datasource_name) unless id
50
+ datasource_name ||= 'default'
51
+ return @datasources[datasource_name] if @datasources[datasource_name]
54
52
 
55
- id
53
+ raise DatasourceDoesNotExistError.new('name', datasource_name)
56
54
  end
57
55
 
58
56
  # Returns if the given datasource ID exists for the grafana instance.
59
57
  #
60
58
  # @return [Boolean] true if exists, false otherwise
61
59
  def datasource_id_exists?(datasource_id)
62
- datasources.value?(datasource_id)
60
+ @datasources.value?(datasource_id)
63
61
  end
64
62
 
65
63
  # @param dashboard_uid [String] UID of the searched {Dashboard}
@@ -97,6 +95,13 @@ module Grafana
97
95
  if @base_uri =~ /^https/
98
96
  http.use_ssl = true
99
97
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
98
+ if @ssl_cert && !File.exist?(@ssl_cert)
99
+ @logger.warn('SSL certificate file does not exist.')
100
+ elsif @ssl_cert
101
+ http.cert_store = OpenSSL::X509::Store.new
102
+ http.cert_store.set_default_paths
103
+ http.cert_store.add_file(@ssl_cert)
104
+ end
100
105
  end
101
106
  http.read_timeout = timeout.to_i if timeout
102
107
 
@@ -112,19 +117,17 @@ module Grafana
112
117
 
113
118
  private
114
119
 
115
- def datasources
116
- if @datasources.nil?
117
- # load datasources from grafana directly, if allowed
118
- response = execute_http_request('/api/datasources')
119
- if response['message'].nil?
120
- json = JSON.parse(response.body)
121
- # only store needed values
122
- @datasources = json.map { |item| [item['name'], item['id']] }.to_h
123
- else
124
- @datasources = {}
125
- end
120
+ def initialize_datasources
121
+ @datasources = {}
122
+
123
+ settings = execute_http_request('/api/frontend/settings')
124
+ return unless settings.is_a?(Net::HTTPOK)
125
+
126
+ json = JSON.parse(settings.body)
127
+ json['datasources'].select { |_k, v| v['id'].to_i.positive? }.each do |ds_name, ds_value|
128
+ @datasources[ds_name] = ds_value['id'].to_i
126
129
  end
127
- @datasources
130
+ @datasources['default'] = @datasources[json['defaultDatasource']]
128
131
  end
129
132
  end
130
133
  end
@@ -33,7 +33,11 @@ module GrafanaReporter
33
33
  action_wizard = false
34
34
 
35
35
  parser = OptionParser.new do |opts|
36
- opts.banner = "Usage: ruby #{$PROGRAM_NAME} [options]"
36
+ if ENV['OCRA_EXECUTABLE']
37
+ opts.banner = "Usage: #{ENV['OCRA_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '')} [options]"
38
+ else
39
+ opts.banner = "Usage: #{Gem.ruby} #{$PROGRAM_NAME} [options]"
40
+ end
37
41
 
38
42
  opts.on('-c', '--config CONFIG_FILE_NAME', 'Specify custom configuration file,'\
39
43
  " instead of #{CONFIG_FILE}.") do |file_name|
@@ -48,11 +52,22 @@ module GrafanaReporter
48
52
  tmp_config.set_param('to_file', file)
49
53
  end
50
54
 
51
- opts.on('-s', '--set VARIABLE,VALUE', Array, 'Set a variable value, which will be passed to the rendering') do |list|
52
- raise ParameterValueError.new(list.length) unless list.length == 2
55
+ opts.on('-s', '--set VARIABLE,VALUE', Array, 'Set a variable value, which will be passed to the '\
56
+ 'rendering') do |list|
57
+ raise ParameterValueError, list.length unless list.length == 2
58
+
53
59
  tmp_config.set_param("default-document-attributes:#{list[0]}", list[1])
54
60
  end
55
61
 
62
+ opts.on('--ssl-cert FILE', 'Manually specify a SSL cert file for HTTPS connection to grafana. Only '\
63
+ 'needed if not working properly otherwise.') do |file|
64
+ if File.exist?(file)
65
+ tmp_config.set_param('grafana-reporter:ssl-cert', file)
66
+ else
67
+ config.logger.warn("SSL certificate file #{file} does not exist. Setting will be ignored.")
68
+ end
69
+ end
70
+
56
71
  opts.on('--test GRAFANA_INSTANCE', 'test current configuration against given GRAFANA_INSTANCE') do |instance|
57
72
  tmp_config.set_param('grafana-reporter:run-mode', 'test')
58
73
  tmp_config.set_param('grafana-reporter:test-instance', instance)
@@ -80,7 +95,7 @@ module GrafanaReporter
80
95
 
81
96
  begin
82
97
  parser.parse!(params)
83
- return config_wizard(config_file) if action_wizard
98
+ return ConsoleConfigurationWizard.new.start_wizard(config_file, tmp_config) if action_wizard
84
99
  rescue ApplicationError => e
85
100
  puts e.message
86
101
  return -1
@@ -122,7 +137,7 @@ module GrafanaReporter
122
137
  when Configuration::MODE_CONNECTION_TEST
123
138
  res = Grafana::Grafana.new(config.grafana_host(config.test_instance),
124
139
  config.grafana_api_key(config.test_instance),
125
- logger: config.logger).test_connection
140
+ logger: config.logger, ssl_cert: config.ssl_cert).test_connection
126
141
  puts res
127
142
 
128
143
  when Configuration::MODE_SINGLE_RENDER
@@ -138,271 +153,6 @@ module GrafanaReporter
138
153
 
139
154
  0
140
155
  end
141
-
142
- private
143
-
144
- # Provides a command line configuration wizard for setting up the necessary configuration
145
- # file.
146
- def config_wizard(config_file)
147
- if File.exist?(config_file)
148
- input = nil
149
- until input
150
- input = user_input("Configuration file '#{config_file}' already exists. Do you want to overwrite it?", 'yN')
151
- return if input =~ /^(?:n|N|yN)$/
152
- end
153
- end
154
-
155
- puts 'This wizard will guide you through an initial configuration for'\
156
- ' the ruby-grafana-reporter. The configuration file will be created'\
157
- ' in the current folder. Please make sure to specify necessary paths'\
158
- ' either with a relative or an absolute path properly.'
159
- puts
160
- puts "Wizard is creating configuration file '#{config_file}'."
161
- puts
162
- port = ui_config_port
163
- grafana = ui_config_grafana
164
- templates = ui_config_templates_folder
165
- reports = ui_config_reports_folder
166
- images = ui_config_images_folder(templates)
167
- retention = ui_config_retention
168
-
169
- config_yaml = %(# This configuration has been built with the configuration wizard.
170
-
171
- #{grafana}
172
-
173
- grafana-reporter:
174
- report-class: GrafanaReporter::Asciidoctor::Report
175
- templates-folder: #{templates}
176
- reports-folder: #{reports}
177
- report-retention: #{retention}
178
- webservice-port: #{port}
179
-
180
- default-document-attributes:
181
- imagesdir: #{images}
182
- # feel free to add here additional asciidoctor document attributes which are applied to all your templates
183
- )
184
-
185
- begin
186
- File.write(config_file, config_yaml, mode: 'w')
187
- puts 'Configuration file successfully created.'
188
- rescue StandardError => e
189
- raise e
190
- end
191
-
192
- config = Configuration.new
193
- begin
194
- config.config = YAML.load_file(config_file)
195
- puts 'Configuration file validated successfully.'
196
- rescue StandardError => e
197
- raise ConfigurationError, "Could not read config file '#{config_file}' (Error: #{e.message})\n"\
198
- "Source:\n#{File.read(config_file)}"
199
- end
200
-
201
- # create a demo report
202
- unless Dir.exist?(config.templates_folder)
203
- puts "Skip creation of DEMO template, as folder '#{config.templates_folder}' does not exist."
204
- return
205
- end
206
- demo_report = %(= First Grafana Report Template
207
-
208
- include::grafana_help[]
209
-
210
- include::grafana_environment[])
211
-
212
- demo_report_file = "#{config.templates_folder}demo_report.adoc"
213
- if File.exist?(demo_report_file)
214
- puts "Skip creation of DEMO template, as file '#{demo_report_file}' already exists."
215
- else
216
- begin
217
- File.write(demo_report_file, demo_report, mode: 'w')
218
- puts "DEMO template '#{demo_report_file}' successfully created."
219
- rescue StandardError => e
220
- raise e
221
- end
222
- end
223
-
224
- puts
225
- puts 'Now everything is setup properly. To create an initial report including a manual of all reporter '\
226
- 'capabilities with the newly created configuration, call the following command:'
227
- puts
228
- puts " ruby-grafana-reporter -c #{config_file} -t #{demo_report_file} -o demo_report_with_help.pdf"
229
- puts
230
- puts 'To start the reporter as a service, call the following command:'
231
- puts
232
- puts " ruby-grafana-reporter -c #{config_file}"
233
- puts
234
- puts "Open 'http://localhost:#{config.webserver_port}/render?var-template=demo_report' in a webbrowser to"
235
- puts 'verify your configuration.'
236
- end
237
-
238
- def ui_config_grafana
239
- valid = false
240
- url = nil
241
- api_key = nil
242
- datasources = ''
243
- until valid
244
- url ||= user_input('Specify grafana host', 'http://localhost:3000')
245
- print "Testing connection to '#{url}' #{api_key ? '_with_' : '_without_'} API key..."
246
- begin
247
- res = Grafana::Grafana.new(url,
248
- api_key,
249
- logger: config.logger).test_connection
250
- rescue StandardError => e
251
- puts
252
- puts e.message
253
- end
254
- puts 'done.'
255
-
256
- case res
257
- when 'Admin'
258
- valid = true
259
-
260
- when 'NON-Admin'
261
- print 'Access to grafana is permitted as NON-Admin. Do you want to use an [a]pi key,'\
262
- ' configure [d]atasource manually, [r]e-enter api key or [i]gnore? [adRi]: '
263
-
264
- case gets
265
- when /(?:i|I)$/
266
- valid = true
267
-
268
- when /(?:a|A)$/
269
- print 'Enter API key: '
270
- api_key = gets.sub(/\n$/, '')
271
-
272
- when /(?:r|R|adRi)$/
273
- api_key = nil
274
-
275
- when /(?:d|D)$/
276
- valid = true
277
- datasources = ui_config_datasources
278
-
279
- end
280
-
281
- else
282
- print "Grafana could not be accessed at '#{url}'. Do you want do [r]e-enter url, or"\
283
- ' [i]gnore and proceed? [Ri]: '
284
-
285
- case gets
286
- when /(?:i|I)$/
287
- valid = true
288
-
289
- else
290
- url = nil
291
- api_key = nil
292
-
293
- end
294
-
295
- end
296
- end
297
- %(grafana:
298
- default:
299
- host: #{url}#{api_key ? "\n api_key: #{api_key}" : ''}#{datasources ? "\n#{datasources}" : ''}
300
- )
301
- end
302
-
303
- def ui_config_datasources
304
- finished = false
305
- datasources = []
306
- until finished
307
- item = {}
308
- print "Datasource ###{datasources.length + 1}) Enter datasource name as configured in grafana: "
309
- item[:ds_name] = gets.sub(/\n$/, '')
310
- print "Datasource ###{datasources.length + 1}) Enter datasource id: "
311
- item[:ds_id] = gets.sub(/\n$/, '')
312
-
313
- puts
314
- selection = user_input("Datasource name: '#{item[:ds_name]}', Datasource id: '#{item[:ds_id]}'."\
315
- ' [A]ccept, [r]etry or [c]ancel?', 'Arc')
316
-
317
- case selection
318
- when /(?:Arc|A|a)$/
319
- datasources << item
320
- another = user_input('Add [a]nother datasource or [d]one?', 'aD')
321
- finished = true if another =~ /(?:d|D)$/
322
-
323
- when /(?:c|C)$/
324
- finished = true
325
-
326
- end
327
- end
328
- " datasources:\n#{datasources.collect { |el| " #{el[:ds_name]}: #{el[:ds_id]}" }.join('\n')}"
329
- end
330
-
331
- def ui_config_port
332
- input = nil
333
- until input
334
- input = user_input('Specify port on which reporter shall run', '8815')
335
- input = nil unless input =~ /[0-9]+/
336
- end
337
- input
338
- end
339
-
340
- def ui_config_templates_folder
341
- input = nil
342
- until input
343
- input = user_input('Specify path where templates shall be stored', './templates')
344
- input = nil unless validate_config_folder(input)
345
- end
346
- input
347
- end
348
-
349
- def ui_config_reports_folder
350
- input = nil
351
- until input
352
- input = user_input('Specify path where created reports shall be stored', './reports')
353
- input = nil unless validate_config_folder(input)
354
- end
355
- input
356
- end
357
-
358
- def ui_config_images_folder(parent)
359
- input = nil
360
- until input
361
- input = user_input('Specify path where rendered images shall be stored (relative to templates folder)',
362
- './images')
363
- input = nil unless validate_config_folder(File.join(parent, input))
364
- end
365
- input
366
- end
367
-
368
- def ui_config_retention
369
- input = nil
370
- until input
371
- input = user_input('Specify report retention duration in hours', '24')
372
- input = nil unless input =~ /[0-9]+/
373
- end
374
- input
375
- end
376
-
377
- def user_input(text, default)
378
- print "#{text} [#{default}]: "
379
- input = gets.gsub(/\n$/, '')
380
- input = default if input.empty?
381
- input
382
- end
383
-
384
- def validate_config_folder(folder)
385
- return true if Dir.exist?(folder)
386
-
387
- print "Directory '#{folder} does not exist: [c]reate, [r]e-enter path or [i]gnore? [cRi]: "
388
- case gets
389
- when /^(?:c|C)$/
390
- begin
391
- Dir.mkdir(folder)
392
- puts "Directory '#{folder}' successfully created."
393
- return true
394
- rescue StandardError => e
395
- puts "WARN: Directory '#{folder}' could not be created. Please create it manually."
396
- puts e.message
397
- end
398
-
399
- when /^(?:i|I)$/
400
- puts "WARN: Directory '#{folder}' does not exist. Please create manually."
401
- return true
402
- end
403
-
404
- false
405
- end
406
156
  end
407
157
  end
408
158
  end