lm_rest 1.0.1

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.
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'lm_rest'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require 'pry'
11
+ Pry.start
data/bin/ds_checker.rb ADDED
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #
4
+ # Based on a script written originally by Matt Dunham
5
+ #
6
+ require 'lm_rest'
7
+ require 'colorize'
8
+
9
+ def usage
10
+ puts "USAGE:\t" + $PROGRAM_NAME + ' account userid passwd datasource_name_or_glob'
11
+ end
12
+
13
+ if ARGV.length == 4
14
+ @account = ARGV[0]
15
+ @userid = ARGV[1]
16
+ @passwd = ARGV[2]
17
+ @lm = LMRest.new(@account, @userid, @passwd)
18
+ else
19
+ usage
20
+ fail 'Bad arguments.'
21
+ end
22
+
23
+ def dp_type(datapoint)
24
+ datapoint.postProcessorMethod == 'expression' ? 'complex' : 'normal'
25
+ end
26
+
27
+ def test_datasource_name(datasource)
28
+ errors = []
29
+
30
+ # does the name contain whitespace?
31
+ errors.push('datasource name contains whitespace') if datasource.name =~ /\s+/
32
+
33
+ # does the name end in a trailing dash?
34
+ errors.push('datasource name has trailing dash') if datasource.name =~ /\-$/
35
+
36
+ # does the display name end in a trailing dash?
37
+ if datasource.displayName =~ /\-$/
38
+ errors.push('datasource display name has trailing dash')
39
+ end
40
+
41
+ errors
42
+ end
43
+
44
+ def test_datasource_description(datasource)
45
+ error = nil
46
+
47
+ # is the description size less than 10 characters in length?
48
+ if datasource.description.length < 10
49
+ error = 'datasource description is empty or sparse'
50
+ end
51
+
52
+ error
53
+ end
54
+
55
+ def test_datapoint_descriptions(datapoints)
56
+ errors = []
57
+
58
+ datapoints.each do |datapoint|
59
+ if datapoint.description.length < 10
60
+ errors.push("datapoint \"" + datapoint.name + "\" description is empty or sparse")
61
+ end
62
+ end
63
+
64
+ errors
65
+ end
66
+
67
+ def test_datapoint_alerts(datapoints)
68
+ errors = []
69
+
70
+ tokens = [
71
+ '##HOST##',
72
+ '##VALUE##',
73
+ '##DURATION##',
74
+ '##START##'
75
+ ]
76
+
77
+ datapoints.each do |datapoint|
78
+ # is there a datapoint alert trigger set, but no custom alert message
79
+ #
80
+ if (datapoint.alertExpr.size > 0) && (datapoint.alertBody == 0)
81
+ errors.push("datapoint \"" + datapoint.name +
82
+ "\" has an alert threshold but no message")
83
+ end
84
+
85
+ # is there a custom alert message on this datapoint?
86
+ #
87
+ next unless datapoint.alertBody.size > 0
88
+ tokens.each do |token|
89
+ # is this token in the datasource definition?
90
+ #
91
+ unless datapoint.alertBody.include? token
92
+ errors.push("custom alert message on \"" + datapoint.name +
93
+ "\" datpoint doesn't include token " + token)
94
+ end
95
+ end
96
+ end
97
+
98
+ errors
99
+ end
100
+
101
+ def test_datapoint_usage(datapoints, complex_datapoints, graphs, overview_graphs)
102
+ errors = []
103
+ datapoint_ok = []
104
+
105
+ puts 'Datapoints:'
106
+ # is an alert trigger set?
107
+ datapoints.each do |datapoint|
108
+ if datapoint.alertExpr.size > 0
109
+ puts ' - ' + dp_type(datapoint) + ' datapoint "' + datapoint.name + '" has alert threshold set'
110
+ else
111
+
112
+ # is this datapoint used in a complex datapoint?
113
+ complex_datapoints.each do |complex_datapoint|
114
+ next unless complex_datapoint.postProcessorParam.include? datapoint.name
115
+ puts(' - ' + dp_type(datapoint) + ' datapoint "' + datapoint.name +
116
+ '" used in complex datapoint' + complex_datapoint.name)
117
+ datapoint_ok.push(datapoint.name)
118
+ break
119
+ end
120
+
121
+ # is this datapoint used in any graphs?
122
+ graphs.each do |graph|
123
+ graph.dataPoints.each do |graph_datapoint|
124
+ next unless datapoint.name == graph_datapoint['name']
125
+ puts(' - ' + dp_type(datapoint) + ' datapoint "' + datapoint.name +
126
+ '" used in graph "' + graph.name + '" datapoint')
127
+ datapoint_ok.push(datapoint.name)
128
+ break
129
+ end
130
+ end
131
+
132
+ overview_graphs.each do |ograph|
133
+ ograph.dataPoints.each do |ograph_datapoint|
134
+ next unless datapoint.name == ograph_datapoint['name']
135
+ puts(' - ' + dp_type(datapoint) + ' datapoint "' + datapoint.name +
136
+ '" used in overview graph "' + ograph.name + '" datapoint')
137
+ datapoint_ok.push(datapoint.name)
138
+ break
139
+ end
140
+ end
141
+
142
+ unless datapoint_ok.include? datapoint.name
143
+ errors.push("datapoint \"" + datapoint.name + "\" appears to be unused")
144
+ end
145
+ end
146
+ end
147
+
148
+ separator
149
+ errors
150
+ end
151
+
152
+ def test_graphs(graphs)
153
+ errors = []
154
+
155
+ puts 'Graphs:'
156
+
157
+ display_prios = {}
158
+ graphs.each do |graph|
159
+ puts ' - "' + graph.name + '" at display priority ' + graph.displayPrio.to_s
160
+ # does the y-axis label contain capital letters?
161
+ if graph.verticalLabel.match(/[A-Z]/)
162
+ errors.push('graph "' + graph.name +
163
+ '" has uppercase letters in the y-axis definition (' +
164
+ graph.verticalLabel + ')')
165
+ end
166
+
167
+ # has this graph priority already been used?
168
+ if display_prios.include? graph.displayPrio
169
+ errors.push('graph "' + graph.name + '" is assigned the same display priority (' +
170
+ graph.displayPrio.to_s + ') as "' +
171
+ display_prios[graph.displayPrio] + '"')
172
+ else
173
+ # no -- store this priority in a hash for further testing
174
+ display_prios[graph.displayPrio] = graph.name
175
+ end
176
+ end
177
+
178
+ separator
179
+ errors
180
+ end
181
+
182
+ def test_overview_graphs(overview_graphs)
183
+ errors = []
184
+
185
+ puts 'Overview Graphs:'
186
+
187
+ display_prios = {}
188
+ overview_graphs.each do |ograph|
189
+ puts ' - "' + ograph.name + '" at display priority ' + ograph.displayPrio.to_s
190
+ if ograph.verticalLabel.match(/[A-Z]/)
191
+ errors.push('overview graph "' + ograph.name +
192
+ '" has uppercase letters in the y-axis definition (' +
193
+ ograph.verticalLabel + ')')
194
+ end
195
+
196
+ if display_prios.include? ograph.displayPrio
197
+ errors.push('overview graph "' + ograph.name + '" is assigned the same display priority (' +
198
+ ograph.displayPrio.to_s + ') as "' +
199
+ display_prios[ograph.displayPrio] + '"')
200
+ else
201
+ display_prios[ograph.displayPrio] = ograph.name
202
+ end
203
+ end
204
+
205
+ separator
206
+ errors
207
+ end
208
+
209
+ def summarize(datasource, datapoints, graphs, overview_graphs)
210
+ datapoint_alert_count = 0
211
+ datapoints.each do |datapoint|
212
+ datapoint_alert_count += 1 if datapoint.alertExpr.size > 0
213
+ end
214
+
215
+ puts 'Summary:'
216
+
217
+ puts " - datasource name:\t#{datasource.name}"
218
+ puts " - display name:\t#{datasource.displayName}"
219
+ puts " - applies to:\t\t#{datasource.appliesTo}"
220
+ puts " - polling interval:\t#{datasource.collectInterval / 60}m"
221
+ puts " - multipoint instance:\t#{datasource.hasMultiInstances}"
222
+ puts " - datapoints:\t\t#{datasource.dataPoints.count}"
223
+ puts " - datapoint alerts:\t#{datapoint_alert_count}"
224
+ puts " - graphs:\t\t#{graphs.count}"
225
+ puts " - overview graphs:\t#{overview_graphs.count}"
226
+
227
+ separator
228
+ end
229
+
230
+ def propose_fixes(errors)
231
+ puts 'Proposed Fixes:'
232
+
233
+ errors.flatten.each do |error|
234
+ puts " * #{error}".colorize(:red)
235
+ end
236
+ end
237
+
238
+ def separator
239
+ puts '============================='
240
+ end
241
+
242
+ @datasources = @lm.get_datasources(filter: "name:#{ARGV[3]}")
243
+
244
+ @datasources.each do |datasource|
245
+ errors = []
246
+ datapoints = @lm.get_datapoints(datasource.id)
247
+ complex_datapoints = datapoints.select { |dp| dp.postProcessorMethod == 'expression' }
248
+ graphs = @lm.get_graphs(datasource.id)
249
+ overview_graphs = @lm.get_overview_graphs(datasource.id)
250
+
251
+ summarize(datasource, datapoints, graphs, overview_graphs)
252
+ errors << test_datasource_name(datasource)
253
+ errors << test_datapoint_descriptions(datapoints)
254
+ errors << test_datapoint_alerts(datapoints)
255
+ errors << test_datapoint_usage(datapoints, complex_datapoints, graphs, overview_graphs)
256
+ errors << test_graphs(graphs)
257
+ errors << test_overview_graphs(overview_graphs)
258
+ propose_fixes(errors)
259
+
260
+ separator
261
+ end
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/lib/lm_rest.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'lm_rest/version'
2
+ require 'lm_rest/api_client'
3
+ require 'lm_rest/resource'
4
+ require 'lm_rest/request_params'
5
+
6
+ module LMRest
7
+ end
@@ -0,0 +1,326 @@
1
+ require 'date'
2
+ require 'base64'
3
+ require 'openssl'
4
+ require 'rest-client'
5
+ require 'json'
6
+ require 'lm_rest/resource'
7
+ require 'lm_rest/request_params'
8
+
9
+ module LMRest
10
+ class APIClient
11
+ include RequestParams
12
+
13
+ ITEMS_SIZE_LIMIT = 1000
14
+ ITEMS_SIZE_DEFAULT = 50
15
+
16
+ BASE_URL_PREFIX = 'https://'
17
+ BASE_URL_SUFFIX = '.logicmonitor.com/santaba/rest'
18
+
19
+ attr_reader :company, :api_url, :access_id
20
+
21
+ def initialize(company = nil, access_id = nil, access_key = nil)
22
+ APIClient.setup
23
+ @company = company
24
+ @access_id = access_id
25
+ @access_key = access_key
26
+ @api_url = BASE_URL_PREFIX + company + BASE_URL_SUFFIX
27
+ end
28
+
29
+ def uri_to_resource_uri(uri)
30
+ # Split the URL down to the resource
31
+ #
32
+ # Here's an example of the process:
33
+ # /setting/datasources/1/graphs?key-value&
34
+ # /setting/datasources/1/graphs
35
+ # /setting/datasources/
36
+ # /setting/datasources
37
+ #
38
+ uri.split("?")[0].split("/").join("/")
39
+ end
40
+
41
+ def snakerize(string)
42
+ string.gsub(/::/, '/').
43
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
44
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
45
+ tr("-", "_").
46
+ downcase
47
+ end
48
+
49
+ def sign(method, uri, data = nil)
50
+
51
+ resource_uri = uri_to_resource_uri(uri)
52
+
53
+ time = DateTime.now.strftime('%Q')
54
+
55
+ http_method = method.to_s.upcase
56
+
57
+ if data.nil? || data.empty?
58
+ data = ''
59
+ else
60
+ data = data.to_json.to_s
61
+ end
62
+
63
+ message = "#{http_method}#{time}#{data}#{resource_uri}"
64
+
65
+ signature = Base64.strict_encode64(
66
+ OpenSSL::HMAC.hexdigest(
67
+ OpenSSL::Digest.new('sha256'),
68
+ access_key,
69
+ message
70
+ )
71
+ )
72
+
73
+ "LMv1 #{access_id}:#{signature}:#{time}"
74
+ end
75
+
76
+ def request(method, uri, params={})
77
+ headers = {}
78
+ headers['Authorization'] = sign(method, uri, params)
79
+ headers['Content-Type'] = 'application/json'
80
+ headers['Accept'] = 'application/json, text/javascript'
81
+ headers['X-version'] = '2'
82
+
83
+ url = api_url + uri
84
+ #puts "URL: " + url
85
+ #puts headers
86
+
87
+ json_params = params.to_json
88
+
89
+ case method
90
+ when :get
91
+ response = RestClient.get(url, headers)
92
+ when :post
93
+ response = RestClient.post(url, json_params, headers)
94
+ when :put
95
+ response = RestClient.put(url, json_params, headers)
96
+ when :delete
97
+ response = RestClient.delete(url, headers: headers)
98
+ end
99
+
100
+ if response.code != 200
101
+ puts response.code.to_s + ":" + response.body.to_s
102
+ raise
103
+ end
104
+
105
+
106
+ JSON.parse(response.body)
107
+ end
108
+
109
+ # Handles making multiple requests to the API if pagination is necessary.
110
+ # Pagination is transparent, and simplifies requests that result in more
111
+ # than ITEMS_SIZE_LIMIT being returned.
112
+ #
113
+ # If you need to walk through resources page-by-page manullay, use the
114
+ # request() method with the 'offset' and 'size' params
115
+ #
116
+ def paginate(uri, params)
117
+
118
+ # Hooray for pagination logic!
119
+ if (params[:size] == 0 || params[:size].nil? || params[:size] > ITEMS_SIZE_LIMIT)
120
+ # save user-entered size in a param for use later
121
+ user_size = params[:size]
122
+
123
+ # set our size param to the max
124
+ params[:size] = ITEMS_SIZE_LIMIT
125
+
126
+ # Set our offset to grab the first page of results
127
+ params[:offset] ||= 0
128
+
129
+ # make the initial request
130
+ body = request(:get, uri.call(params), nil)
131
+
132
+ # pull the actual items out of the request body and into our
133
+ # item_collector while we build up the items list
134
+ item_collector = body['items']
135
+
136
+ # The API sends the total number of objects back in the first request.
137
+ # We need this to determine how many more pages to pull
138
+ total = body['total']
139
+
140
+ # If user didn't pass size param, set it to total
141
+ # This just means you'll get all items if not specifying a size
142
+ user_size ||= total
143
+
144
+ # If the user passed a size larger than what's available, set it to
145
+ # total to retrieve all items
146
+ if user_size > total
147
+ user_size = total
148
+ end
149
+
150
+ # calculate the remaining number of items (after first request)
151
+ # then use that to figure out how many more times we need to call
152
+ # request() to get all the items, then do it
153
+ pages_remaining = ((user_size - ITEMS_SIZE_LIMIT).to_f/ITEMS_SIZE_LIMIT).ceil
154
+
155
+ pages_remaining.times do |page|
156
+
157
+ # Increment the offset by the limit to get the next page
158
+ params[:offset] += ITEMS_SIZE_LIMIT
159
+
160
+ # if this is the last page, get the remainder
161
+ if page == pages_remaining - 1
162
+ params[:size] = user_size%ITEMS_SIZE_LIMIT
163
+ else
164
+ # else, get a whole page
165
+ params[:size] = ITEMS_SIZE_LIMIT
166
+ end
167
+
168
+ # make a subsequent request with modified params
169
+ body = request(:get, uri.call(params), nil)
170
+
171
+ # add these items to our item_collector
172
+ item_collector += body['items']
173
+ end
174
+
175
+ body['items'] = item_collector
176
+ body
177
+ else
178
+ # No pagination required, just request the page
179
+ request(:get, uri.call(params), nil)
180
+ end
181
+ end
182
+
183
+ def self.process_paths
184
+ resource_uri = attributes['url']
185
+ @@api_json[paths].keys.each do |path|
186
+
187
+ path.keys.each do |action|
188
+ case action
189
+ when 'get'
190
+
191
+ uri = lambda { |params| "#{resource_uri}#{RequestParams.parameterize(params)}"}
192
+ method_name = snakerize(@@api_json['paths'][path][action][operationId])
193
+
194
+ unless plural.nil?
195
+ # Define a method to fetch multiple resources with optional params
196
+ define_method("get_#{plural}") do |params = {}|
197
+ Resource.parse paginate(uri, params)
198
+ end
199
+ end
200
+
201
+ # Define a method to get one resource by it's id number, with optional
202
+ # params, thought now that I think about it I'm not sure why you'd pass
203
+ # params when grabbing just one resource.
204
+
205
+ # Some resources are Singletons
206
+ unless singular.nil?
207
+ define_method("get_#{singular}") do |*args|
208
+ case args.size
209
+ when 0
210
+ Resource.parse request(:get, "#{resource_uri}", nil)
211
+ when 1
212
+ Resource.parse request(:get, "#{resource_uri}/#{args[0]}", nil)
213
+ when 2
214
+ Resource.parse request(:get, "#{resource_uri}/#{args[0]}#{RequestParams.parameterize(args[1])}", nil)
215
+ else
216
+ raise ArgumentError.new("wrong number for arguments (#{args.count} for 1..2)")
217
+ end
218
+ end
219
+ end
220
+
221
+ when 'add'
222
+
223
+ # Define a method to add a new resource to the account
224
+ define_method("add_#{singular}") do |properties|
225
+ if properties.class == LMRest::Resource
226
+ Resource.parse request(:post, "#{resource_uri}", properties.to_h)
227
+ else
228
+ Resource.parse request(:post, "#{resource_uri}", properties)
229
+ end
230
+ end
231
+
232
+ when 'update'
233
+
234
+ # Define a method to update a resource
235
+ define_method("update_#{singular}") do |id, properties = {}|
236
+ if id.class == LMRest::Resource
237
+ Resource.parse request(:put, "#{resource_uri}/#{id.id}", id.to_h)
238
+ else
239
+ Resource.parse request(:put, "#{resource_uri}/#{id}", properties)
240
+ end
241
+ end
242
+
243
+ when 'delete'
244
+
245
+ # Define a method to delete the resource
246
+ define_method("delete_#{singular}") do |id|
247
+ if id.class == LMRest::Resource
248
+ id = id.id
249
+ Resource.parse request(:delete, "#{resource_uri}/#{id}", nil)
250
+ else
251
+ Resource.parse request(:delete, "#{resource_uri}/#{id}", nil)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ def self.define_child_methods(resource_type, attributes)
260
+ parent_singular = attributes['method_names']['singular']
261
+ parent_plural = attributes['method_names']['plural']
262
+ parent_resource_uri = attributes['url']
263
+ parent_id = attributes['parent_id_key']
264
+ children = attributes['children']
265
+
266
+ children.each do |child_name|
267
+ if @@api_json[child_name]
268
+ child = @@api_json[child_name]
269
+ else
270
+ raise "Child resource " + child_name + " not defined."
271
+ end
272
+
273
+ child_singular = child['method_names']['singular']
274
+ child_plural = child['method_names']['plural']
275
+ child_resource_uri = attributes['url'].split("/").last
276
+
277
+ child['actions'].each do |action|
278
+ case action
279
+ when 'get'
280
+
281
+ define_method("get_#{parent_singular}_#{child_plural}") do |id, params = {}, &block|
282
+ uri = lambda { |params| "#{parent_resource_uri}/#{id}/#{child['method_names']['plural']}#{RequestParams.parameterize(params)}" }
283
+ Resource.parse paginate(uri, params)
284
+ end
285
+
286
+ when 'add'
287
+ when 'update'
288
+ when 'delete'
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ # Define methods based on the JSON structure
295
+ def self.setup
296
+ @@api_definition_path = File.expand_path(File.join(File.dirname(__FILE__), "../../api.json"))
297
+ @@api_json = JSON.parse(File.read(@@api_definition_path))
298
+ @@api_json.each do |resource_type, attributes|
299
+ define_action_methods(resource_type, attributes) if attributes['actions']
300
+ define_child_methods(resource_type, attributes) if attributes['children']
301
+ end
302
+ end
303
+
304
+ # Ack a down collector, pass the ID and a comment
305
+ def ack_collector_down(id, comment)
306
+ if id.class == LMRest::Resource
307
+ Resource.parse request(:post, "/setting/collectors/#{id.id}/ackdown", {comment: comment})
308
+ else
309
+ Resource.parse request(:post, "/setting/collectors/#{id}/ackdown", {comment: comment})
310
+ end
311
+ end
312
+
313
+ # run a report
314
+ def run_report(id, type = "generateReport")
315
+ if id.class == LMRest::Resource
316
+ Resource.parse request(:post, "/functions", {reportId: id.id, type: type})
317
+ else
318
+ Resource.parse request(:post, "/functions", {reportId: id, type: type})
319
+ end
320
+ end
321
+
322
+ private
323
+
324
+ attr_accessor :access_key
325
+ end
326
+ end