noms-client 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,508 @@
1
+ #!ruby
2
+ # /* Copyright 2014 Evernote Corporation. All rights reserved.
3
+ # Copyright 2013 Proofpoint, Inc. All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ # */
17
+
18
+
19
+ require 'net/http'
20
+ require 'net/https'
21
+ require 'rubygems'
22
+ require 'noms/errors'
23
+ require 'rexml/document'
24
+ require 'json'
25
+
26
+ class Hash
27
+
28
+ # The mkdir -p of []=
29
+ def set_deep(keypath, value)
30
+ if keypath.length == 1
31
+ self[keypath[0]] = value
32
+ else
33
+ self[keypath[0]] ||= Hash.new
34
+ self[keypath[0]].set_deep(keypath[1 .. -1], value)
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ class NOMS
41
+
42
+ end
43
+
44
+ class NOMS::XmlHash < Hash
45
+
46
+ attr_accessor :element, :name
47
+
48
+ def initialize(el)
49
+ super
50
+ @element = el
51
+ @name = el.name
52
+ self['text'] = el.text
53
+ el.attributes.each do |attr, value|
54
+ self[attr] = value
55
+ end
56
+ self['children'] = []
57
+ el.elements.each do |child|
58
+ self['children'] << NOMS::XmlHash.new(child)
59
+ end
60
+ end
61
+
62
+ def to_xml(name=nil)
63
+ el = REXML::Element.new(name || self.name)
64
+ el.text = self['text'] if self.has_key? 'text'
65
+ self.each do |key, val|
66
+ next if ['children', 'text'].include? key
67
+ el.add_attribute(key, val)
68
+ end
69
+ if self.has_key? 'children'
70
+ self['children'].each do |child|
71
+ el.add_element child.to_xml
72
+ end
73
+ end
74
+ el
75
+ end
76
+
77
+ end
78
+
79
+ class NOMS::HttpClient
80
+
81
+ @@mocked = false
82
+
83
+ def self.mock!(datafile=nil)
84
+ @@mocked = true
85
+ @@mockdata = datafile
86
+ end
87
+
88
+ def self.mockery
89
+ NOMS::HttpClient::RestMock
90
+ end
91
+
92
+ def initialize(opt)
93
+ @opt = opt
94
+ @delegate = (@@mocked ? self.class.mockery.new(self) :
95
+ NOMS::HttpClient::Real.new(self))
96
+ end
97
+
98
+ def opt
99
+ @opt
100
+ end
101
+
102
+ def handle_mock(method, url, opt)
103
+ false
104
+ end
105
+
106
+ def config_key
107
+ 'httpclient'
108
+ end
109
+
110
+ def myconfig(key=nil)
111
+ unless @opt.has_key? config_key and @opt[config_key]
112
+ raise NOMS::Error::Config, "Configuration provided to #{self.class} doesn't have a '#{config_key}' key"
113
+ end
114
+
115
+ cfg = @opt[config_key]
116
+
117
+ if key
118
+ unless cfg.has_key? key
119
+ msg = "#{config_key} configuration doesn't have a '#{key}' property"
120
+ msg += " (#{@opt[config_key].inspect})"
121
+ raise NOMS::Error::Config, msg
122
+ end
123
+ cfg[key]
124
+ else
125
+ cfg
126
+ end
127
+ end
128
+
129
+ # Used mostly for mocking behavior
130
+ def allow_partial_updates
131
+ # Replace during PUT
132
+ false
133
+ end
134
+
135
+ def default_content_type
136
+ 'application/json'
137
+ end
138
+
139
+ def ignore_content_type
140
+ false
141
+ end
142
+
143
+ def dbg(msg)
144
+ if @opt.has_key? 'debug' and @opt['debug'] > 1
145
+ puts "DBG(#{self.class}): #{msg}"
146
+ end
147
+ end
148
+
149
+ def trim(s, c='/', dir=:both)
150
+ case dir
151
+ when :both
152
+ trim(trim(s, c, :right), c, :left)
153
+ when :right
154
+ s.sub(Regexp.new(c + '+/'), '')
155
+ when :left
156
+ s.sub(Regexp.new('^' + c + '+'), '')
157
+ end
158
+ end
159
+
160
+ def ltrim(s, c='/')
161
+ trim(s, c, :left)
162
+ end
163
+
164
+ def rtrim(s, c='/')
165
+ trim(s, c, :right)
166
+ end
167
+
168
+ def method_missing(meth, *args, &block)
169
+ @delegate.send(meth, *args, &block)
170
+ end
171
+
172
+ end
173
+
174
+ class NOMS::HttpClient::RestMock < NOMS::HttpClient
175
+
176
+ def initialize(delegator)
177
+ @delegator = delegator
178
+ @data = { }
179
+ @opt = @delegator.opt
180
+ @opt['return-hash'] = true unless @opt.has_key? 'return-hash'
181
+ self.dbg "Initialized with options: #{opt.inspect}"
182
+ end
183
+
184
+ def allow_partial_updates
185
+ @delegator.allow_partial_updates
186
+ end
187
+
188
+ def config_key
189
+ @delegator.config_key
190
+ end
191
+
192
+ def default_content_type
193
+ @delegator.default_content_type
194
+ end
195
+
196
+ def ignore_content_type
197
+ @delegator.ignore_content_type
198
+ end
199
+
200
+
201
+ def id_field(dummy=nil)
202
+ 'id'
203
+ end
204
+
205
+ def maybe_save
206
+ if @@mockdata
207
+ File.open(@@mockdata, 'w') { |fh| fh << JSON.pretty_generate(@data) }
208
+ end
209
+ end
210
+
211
+ def maybe_read
212
+ if @@mockdata and File.exist? @@mockdata
213
+ @data = File.open(@@mockdata, 'r') { |fh| JSON.load(fh) }
214
+ end
215
+ end
216
+
217
+ def all_data
218
+ maybe_read
219
+ @data
220
+ end
221
+
222
+ def do_request(opt={})
223
+ maybe_read
224
+ method = [:GET, :PUT, :POST, :DELETE].find do |m|
225
+ opt.has_key? m
226
+ end
227
+ method ||= :GET
228
+ opt[method] ||= ''
229
+
230
+ rel_uri = opt[method]
231
+ dbg "relative URI is #{rel_uri}"
232
+ url = URI.parse(myconfig 'url')
233
+ unless opt[method] == ''
234
+ url.path = rtrim(url.path) + '/' + ltrim(rel_uri) unless opt[:absolute]
235
+ end
236
+ url.query = opt[:query] if opt.has_key? :query
237
+ dbg "url=#{url}"
238
+
239
+ handled = handle_mock(method, url, opt)
240
+ return handled if handled
241
+
242
+ # We're not mocking absolute URLs specifically
243
+ case method
244
+
245
+ when :PUT
246
+ # Upsert - whole objects only
247
+ dbg "Processing PUT"
248
+ @data[url.host] ||= { }
249
+ collection_path_components = url.path.split('/')
250
+ id = collection_path_components.pop
251
+ collection_path = collection_path_components.join('/')
252
+ @data[url.host][collection_path] ||= [ ]
253
+ object_index =
254
+ @data[url.host][collection_path].index { |el| el[id_field(collection_path)] == id }
255
+ if object_index.nil?
256
+ object = opt[:body].merge({ id_field(collection_path) => id })
257
+ dbg "creating in collection #{collection_path}: #{object.inspect}"
258
+ @data[url.host][collection_path] << opt[:body].merge({ id_field(collection_path) => id })
259
+ else
260
+ if allow_partial_updates
261
+ object = @data[url.host][collection_path][object_index].merge(opt[:body])
262
+ dbg "updating in collection #{collection_path}: to => #{object.inspect}"
263
+ else
264
+ object = opt[:body].merge({ id_field(collection_path) => id })
265
+ dbg "replacing in collection #{collection_path}: #{object.inspect}"
266
+ end
267
+ @data[url.host][collection_path][object_index] = object
268
+ end
269
+ maybe_save
270
+ object
271
+
272
+ when :POST
273
+ # Insert/Create
274
+ dbg "Processing POST"
275
+ @data[url.host] ||= { }
276
+ collection_path = url.path
277
+ @data[url.host][collection_path] ||= [ ]
278
+ id = opt[:body][id_field(collection_path)] || opt[:body].object_id
279
+ object = opt[:body].merge({id_field(collection_path) => id})
280
+ dbg "creating in collection #{collection_path}: #{object.inspect}"
281
+ @data[url.host][collection_path] << object
282
+ maybe_save
283
+ object
284
+
285
+ when :DELETE
286
+ dbg "Processing DELETE"
287
+ if @data[url.host]
288
+ if @data[url.host].has_key? url.path
289
+ # DELETE on a collection
290
+ @data[url.host].delete url.path
291
+ true
292
+ elsif @data[url.host].has_key? url.path.split('/')[0 .. -2].join('/')
293
+ # DELETE on an object by Id
294
+ path_components = url.path.split('/')
295
+ id = path_components.pop
296
+ collection_path = path_components.join('/')
297
+ object_index = @data[url.host][collection_path].index { |obj| obj[id_field(collection_path)] == id }
298
+ if object_index.nil?
299
+ raise NOMS::Error, "Error (#{self.class} making #{config_key} request " +
300
+ "(404): No such object id (#{id_field(collection_path)} == #{id}) in #{collection_path}"
301
+ else
302
+ @data[url.host][collection_path].delete_at object_index
303
+ end
304
+ maybe_save
305
+ true
306
+ else
307
+ raise NOMS::Error, "Error (#{self.class}) making #{config_key} request " +
308
+ "(404): No objects at location or in collection #{url.path}"
309
+ end
310
+ else
311
+ raise NOMS::Error, "Error (#{self.class}) making #{config_key} request " +
312
+ "(404): No objects on #{url.host}"
313
+ end
314
+
315
+ when :GET
316
+ dbg "Performing GET"
317
+ if @data[url.host]
318
+ dbg "we store data for #{url.host}"
319
+ if @data[url.host].has_key? url.path
320
+ # GET on a collection
321
+ # TODO get on the query string
322
+ dbg "returning collection #{url.path}"
323
+ @data[url.host][url.path]
324
+ elsif @data[url.host].has_key? url.path.split('/')[0 .. -2].join('/')
325
+ # GET on an object by Id
326
+ path_components = url.path.split('/')
327
+ id = path_components.pop
328
+ collection_path = path_components.join('/')
329
+ dbg "searching in collection #{collection_path}: id=#{id}"
330
+ dbg "data: #{@data[url.host][collection_path].inspect}"
331
+ object = @data[url.host][collection_path].find { |obj| obj[id_field(collection_path)] == id }
332
+ if object.nil?
333
+ raise NOMS::Error, "Error (#{self.class} making #{config_key} request " +
334
+ "(404): No such object id (#{id_field(collection_path)} == #{id}) in #{collection_path}"
335
+ end
336
+ dbg " found #{object.inspect}"
337
+ object
338
+ else
339
+ raise NOMS::Error, "Error (#{self.class}) making #{config_key} request " +
340
+ "(404): No objects at location or in collection #{url.path}"
341
+ end
342
+ else
343
+ raise NOMS::Error, "Error (#{self.class}) making #{config_key} request " +
344
+ "(404): No objects on #{url.host}"
345
+ end
346
+ end
347
+ end
348
+
349
+ end
350
+
351
+ class NOMS::HttpClient::Real < NOMS::HttpClient
352
+
353
+ def initialize(delegator)
354
+ @delegator = delegator
355
+ @opt = @delegator.opt
356
+ @opt['return-hash'] = true unless @opt.has_key? 'return-hash'
357
+ self.dbg "Initialized with options: #{opt.inspect}"
358
+ end
359
+
360
+ def config_key
361
+ @delegator.config_key
362
+ end
363
+
364
+ def default_content_type
365
+ @delegator.default_content_type
366
+ end
367
+
368
+ def ignore_content_type
369
+ @delegator.ignore_content_type
370
+ end
371
+
372
+
373
+ def do_request(opt={})
374
+ method = [:GET, :PUT, :POST, :DELETE].find do |m|
375
+ opt.has_key? m
376
+ end
377
+ if method == nil
378
+ method = :GET
379
+ opt[method] = ''
380
+ end
381
+ rel_uri = opt[method]
382
+ opt[:redirect_limit] ||= 10
383
+ if opt[:absolute]
384
+ url = URI.parse(rel_uri)
385
+ else
386
+ url = URI.parse(myconfig 'url')
387
+ unless opt[method] == ''
388
+ url.path = rtrim(url.path) + '/' + ltrim(rel_uri) unless opt[:absolute]
389
+ end
390
+ url.query = opt[:query] if opt.has_key? :query
391
+ end
392
+ self.dbg("#{method.inspect} => #{url.to_s}")
393
+ http = Net::HTTP.new(url.host, url.port)
394
+ http.read_timeout = myconfig.has_key?('timeout') ? myconfig['timeout'] : 120
395
+ http.use_ssl = true if url.scheme == 'https'
396
+ if http.use_ssl?
397
+ self.dbg("using SSL/TLS")
398
+ if myconfig.has_key? 'verify-with-ca'
399
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
400
+ http.ca_file = myconfig['verify-with-ca']
401
+ else
402
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
403
+ end
404
+ else
405
+ self.dbg("NOT using SSL/TLS")
406
+ end
407
+ reqclass = case method
408
+ when :GET
409
+ Net::HTTP::Get
410
+ when :PUT
411
+ Net::HTTP::Put
412
+ when :POST
413
+ Net::HTTP::Post
414
+ when :DELETE
415
+ Net::HTTP::Delete
416
+ end
417
+ request = reqclass.new(url.request_uri)
418
+ self.dbg request.to_s
419
+ if myconfig.has_key? 'username'
420
+ self.dbg "will do basic authentication as #{myconfig['username']}"
421
+ request.basic_auth(myconfig['username'], myconfig['password'])
422
+ else
423
+ self.dbg "no authentication"
424
+ end
425
+ if opt.has_key? :body
426
+ content_type = opt[:content_type] || default_content_type
427
+ request['Content-Type'] = content_type
428
+ request.body = case content_type
429
+ when /json$/
430
+ opt[:body].to_json
431
+ when /xml$/
432
+ opt[:body].to_xml
433
+ else
434
+ opt[:body]
435
+ end
436
+ end
437
+ response = http.request(request)
438
+ self.dbg response.to_s
439
+ if response.is_a? Net::HTTPRedirection
440
+ if opt[:redirect_limit] == 0
441
+ raise "Error (#{self.class}) making #{config_key} request " +
442
+ "(redirect limit exceeded): on #{response['location']}"
443
+ end
444
+ # TODO check if really absolute or make sure it is
445
+ self.dbg "Redirect to #{response['location']}"
446
+ do_request opt.merge({ :GET => response['location'],
447
+ :absolute => true,
448
+ :redirect_limit => opt[:redirect_limit] - 1
449
+ })
450
+ end
451
+
452
+ unless response.is_a? Net::HTTPSuccess
453
+ raise "Error (#{self.class}) making #{config_key} request " +
454
+ "(#{response.code}): " + error_body(response.body)
455
+ end
456
+
457
+ if response.body
458
+ type = ignore_content_type ? default_content_type :
459
+ (response.content_type || default_content_type)
460
+ self.dbg "Response body is type #{type}"
461
+ case type
462
+ when /xml$/
463
+ doc = REXML::Document.new response.body
464
+ if @opt['return-hash']
465
+ _xml_to_hash doc
466
+ else
467
+ doc
468
+ end
469
+ when /json$/
470
+ # Ruby JSON doesn't like bare values in JSON, we'll try to wrap these as
471
+ # one-element array
472
+ bodytext = response.body
473
+ bodytext = '[' + bodytext + ']' unless ['{', '['].include? response.body[0].chr
474
+ JSON.parse(bodytext)
475
+ else
476
+ response.body
477
+ end
478
+ else
479
+ true
480
+ end
481
+ end
482
+
483
+ def _xml_to_hash(rexml)
484
+ NOMS::XmlHash.new rexml.root
485
+ end
486
+
487
+ def error_body(body_text, content_type=nil)
488
+ content_type ||= default_content_type
489
+ begin
490
+ extracted_message = case content_type
491
+ when /json$/
492
+ structure = JSON.parse(body_text)
493
+ structure['message']
494
+ when /xml$/
495
+ REXML::Document.new(body_text).root.elements["//message"].first.text
496
+ else
497
+ Hash.new
498
+ end
499
+ ['message', 'error', 'error_message'].each do |key|
500
+ return structure[key].to_s if structure.has_key? key
501
+ end
502
+ body_text.to_s
503
+ rescue
504
+ body_text.to_s
505
+ end
506
+ end
507
+
508
+ end
data/lib/noms/nagui.rb ADDED
@@ -0,0 +1,57 @@
1
+ #!ruby
2
+ # /* Copyright 2014 Evernote Corporation. All rights reserved.
3
+ # Copyright 2013 Proofpoint, Inc. All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ # */
17
+
18
+ require 'noms/httpclient'
19
+ require 'uri'
20
+
21
+ class NOMS
22
+
23
+ end
24
+
25
+ class NOMS::Nagui < NOMS::HttpClient
26
+ def dbg(msg)
27
+ if @opt.has_key? 'debug' and @opt['debug'] > 2
28
+ puts "DBG(#{self.class}): #{msg}"
29
+ end
30
+ end
31
+ def initialize(opt)
32
+ @opt = opt
33
+ self.dbg "Initialized with options: #{opt.inspect}"
34
+ end
35
+
36
+ def config_key
37
+ 'nagui'
38
+ end
39
+
40
+ def system(hostname)
41
+ results = do_request(:GET => '/nagui/nagios_live.cgi', :query => URI.encode("query=GET hosts|Filter: name ~~ #{hostname}"))
42
+ if results.kind_of?(Array) && results.length > 0
43
+ results[0]
44
+ else
45
+ nil
46
+ end
47
+ end
48
+ def check_host_online(host)
49
+ @opt['host_up_command'] = 'check-host-alive' unless @opt.has_key?('host_up_command')
50
+ nagcheck=do_request(:GET => "/nagcheck/command/#{host}/#{@opt['host_up_command']}")
51
+ if nagcheck['state'] == 0
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ #!ruby
2
+
3
+ require 'rspec/collection_matchers'
4
+
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |c|
7
+ c.syntax = [:should, :expect]
8
+ end
9
+ end
10
+
11
+ def init_test
12
+ if ENV['TEST_DEBUG'] and ENV['TEST_DEBUG'].length > 0
13
+ $debug = (ENV['TEST_DEBUG'].to_i == 0 ? 2 : ENV['TEST_DEBUG'].to_i)
14
+ else
15
+ $debug = 0
16
+ end
17
+
18
+ $server = 'noms-server'
19
+ $cmdbapi = '/cmdb_api/v1'
20
+ $api = '/ncc_api/v2'
21
+ $opt = {
22
+ 'ncc' => { 'url' => "http://#{$server}#{$api}" },
23
+ 'debug' => $debug,
24
+ 'cmdb' => { 'url' => "http://#{$server}#{$cmdbapi}" }
25
+ }
26
+ end
27
+
28
+ init_test
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'noms/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "noms-client"
8
+ spec.version = NOMS::Client::VERSION
9
+ spec.authors = ["Jeremy Brinkley"]
10
+ spec.email = ["jbrinkley@evernote.com"]
11
+ spec.summary = %q{Client libraries and command-line tool for NOMS components}
12
+ spec.homepage = "http://github.com/evernote/noms-client"
13
+ spec.license = "Apache-2"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.7"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "rspec"
23
+ spec.add_development_dependency "rspec-collection_matchers"
24
+
25
+ spec.add_runtime_dependency "optconfig"
26
+ end
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env rspec
2
+
3
+ require 'noms/cmdb'
4
+ require 'spec_helper'
5
+
6
+ describe NOMS::CMDB do
7
+
8
+ before(:all) { init_test }
9
+
10
+ describe "#new" do
11
+
12
+ it "creates a new CMDB client object" do
13
+
14
+ obj = NOMS::CMDB.new($opt)
15
+ obj.should be_a NOMS::CMDB
16
+
17
+ end
18
+
19
+ end
20
+
21
+ end
data/spec/02noms.sh ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bats
2
+
3
+ @test "noms help" {
4
+
5
+ noms --help
6
+
7
+ }
8
+