noms-client 1.9.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.
@@ -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
+