lm_rest 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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