puppetdb-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ vendor
2
+ .bundle
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "httparty"
4
+
5
+ group :test do
6
+ gem 'rspec'
7
+ gem 'mocha'
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ puppetdb-ruby
2
+
3
+ Copyright (C) 2013 Puppet Labs Inc
4
+
5
+ Puppet Labs can be contacted at: info@puppetlabs.com
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,57 @@
1
+ # puppetdb-ruby
2
+
3
+ a simple gem for interacting with the
4
+ [PuppetDB](https://github.com/puppetlabs/puppetdb) API.
5
+
6
+ ## Installation
7
+
8
+ gem install puppetdb-ruby
9
+
10
+ ## Usage
11
+
12
+ ```ruby
13
+ require 'puppetdb'
14
+
15
+ # Defaults to latest API version.
16
+
17
+ # non-ssl
18
+ client = PuppetDB::Client({:server => 'http://localhost:8080'})
19
+
20
+ # ssl
21
+ client = PuppetDB::Client({
22
+ :server => 'https://localhost:8081',
23
+ :pem => {
24
+ :key => "keyfile",
25
+ :cert => "certfile",
26
+ :ca_file => "cafile"
27
+ }})
28
+
29
+ response = client.request([:and,
30
+ [:'=', ['fact', 'kernel'], 'Linux'],
31
+ [:>, ['fact', 'uptime_days'], 30]]], {:limit => 10})
32
+ nodes = response.data
33
+
34
+ # queries are composable
35
+
36
+ uptime = PuppetDB::Query[:>, ['fact', 'uptime_days'], 30]
37
+ redhat = PuppetDB::Query[:'=', ['fact', 'osfamily'], 'RedHat']
38
+ debian = PuppetDB::Query[:'=', ['fact', 'osfamily'], 'Debian']
39
+
40
+ client.request uptime.and(debian)
41
+ client.request uptime.and(redhat)
42
+ client.request uptime.and(debian.or(redhat))
43
+ ```
44
+
45
+ ## tests
46
+
47
+ bundle install
48
+ bundle exec rspec
49
+
50
+ ## Authors
51
+
52
+ Nathaniel Smith <nathaniel@puppetlabs.com>
53
+ Lindsey Smith <lindsey@puppetlabs.com>
54
+
55
+ ## License
56
+
57
+ See LICENSE.
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), 'puppetdb', 'client')
2
+ require File.join(File.dirname(__FILE__), 'puppetdb', 'response')
3
+ require File.join(File.dirname(__FILE__), 'puppetdb', 'query')
@@ -0,0 +1,111 @@
1
+ require 'httparty'
2
+ require 'logger'
3
+
4
+ module PuppetDB
5
+ class APIError < Exception
6
+ attr_reader :code, :response
7
+ def initialize(response)
8
+ @response = response
9
+ end
10
+ end
11
+
12
+ class FixSSLConnectionAdapter < HTTParty::ConnectionAdapter
13
+ def attach_ssl_certificates(http, options)
14
+ http.cert = OpenSSL::X509::Certificate.new(File.read(options[:pem]['cert']))
15
+ http.key = OpenSSL::PKey::RSA.new(File.read(options[:pem]['key']))
16
+ http.ca_file = options[:pem]['ca_file']
17
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
18
+ end
19
+ end
20
+
21
+ class Client
22
+ include HTTParty
23
+ attr_reader :use_ssl
24
+ attr_writer :logger
25
+
26
+ def hash_get(hash, key)
27
+ untouched = hash[key]
28
+ return untouched if untouched
29
+
30
+ sym = hash[key.to_sym()]
31
+ return sym if sym
32
+
33
+ str = hash[key.to_s()]
34
+ return str if str
35
+
36
+ nil
37
+ end
38
+
39
+ def hash_includes?(hash, *sought_keys)
40
+ sought_keys.each {|x| return false unless hash.include?(x)}
41
+ true
42
+ end
43
+
44
+ def debug(msg)
45
+ if @logger
46
+ @logger.debug(msg)
47
+ end
48
+ end
49
+
50
+ def initialize(settings, version=3)
51
+ @version = version
52
+
53
+ server = hash_get(settings, 'server')
54
+ pem = hash_get(settings, 'pem')
55
+
56
+ scheme = URI.parse(server).scheme
57
+
58
+ unless ['http', 'https'].include? scheme
59
+ error_msg = "Configuration error: :server must specify a protocol of either http or https"
60
+ raise error_msg
61
+ end
62
+
63
+ @use_ssl = scheme == 'https'
64
+ if @use_ssl
65
+ unless pem && hash_includes?(pem, 'key', 'cert', 'ca_file')
66
+ error_msg = 'Configuration error: https:// specified but pem is missing or incomplete. It requires cert, key, and ca_file.'
67
+ raise error_msg
68
+ end
69
+
70
+ self.class.default_options = {:pem => pem}
71
+ self.class.connection_adapter(FixSSLConnectionAdapter)
72
+ end
73
+
74
+ self.class.base_uri(server + '/v' + version.to_s())
75
+ end
76
+
77
+ def raise_if_error(response)
78
+ if response.code.to_s() =~ /^[4|5]/
79
+ raise APIError.new(response)
80
+ end
81
+ end
82
+
83
+ def request(endpoint, query, opts={})
84
+ query = PuppetDB::Query.maybe_promote(query)
85
+ json_query = query.build()
86
+
87
+ path = "/" + endpoint
88
+
89
+ filtered_opts = {'query' => json_query}
90
+ opts.each do |k,v|
91
+ if k == :counts_filter
92
+ filtered_opts['counts-filter'] = JSON.dump(v)
93
+ else
94
+ filtered_opts[k.to_s.sub("_", "-")] = v
95
+ end
96
+ end
97
+
98
+ debug("#{path} #{json_query} #{opts}")
99
+
100
+ ret = self.class.get(path, :query => filtered_opts)
101
+ raise_if_error(ret)
102
+
103
+ total = ret.headers['X-Records']
104
+ if total.nil?
105
+ total = ret.parsed_response.length
106
+ end
107
+
108
+ Response.new(ret.parsed_response, total)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,58 @@
1
+ require 'json'
2
+
3
+ module PuppetDB
4
+ class Query
5
+ attr_reader :sexpr
6
+
7
+ def initialize(query=[])
8
+ @sexpr = query
9
+ end
10
+
11
+ def self.[](*args)
12
+ Query.new(args)
13
+ end
14
+
15
+ def self.maybe_promote(query)
16
+ return Query.new(query) unless query.class == Query
17
+ query
18
+ end
19
+
20
+ def empty?
21
+ @sexpr.empty?
22
+ end
23
+
24
+ def compose(query)
25
+ query = self.class.maybe_promote(query)
26
+
27
+ # If an operand is the empty query ([]), compose returns a copy
28
+ # of the non-empty operand. If both operands are empty, the
29
+ # empty query is returned. If both operands are non-empty, the
30
+ # compose continues.
31
+ if query.empty? && !self.empty?
32
+ Query.new(@sexpr)
33
+ elsif self.empty? && !query.empty?
34
+ Query.new(query.sexpr())
35
+ elsif self.empty? && query.empty?
36
+ Query.new([])
37
+ else
38
+ yield query
39
+ end
40
+ end
41
+
42
+ def and(query)
43
+ compose(query) { |q| Query.new([:and, @sexpr, q.sexpr()]) }
44
+ end
45
+
46
+ def or(query)
47
+ compose(query) { |q| Query.new([:or, @sexpr, q.sexpr()]) }
48
+ end
49
+
50
+ def push(query)
51
+ compose(query) { |q| Query.new(@sexpr.dup.push(q.sexpr())) }
52
+ end
53
+
54
+ def build
55
+ JSON.dump(@sexpr)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,10 @@
1
+ module PuppetDB
2
+ class Response
3
+ attr_reader :data, :total_records
4
+
5
+ def initialize(data, total_records=nil)
6
+ @data = data
7
+ @total_records = total_records
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |conf|
2
+ conf.mock_framework = :mocha
3
+ end
@@ -0,0 +1,179 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'puppetdb')
3
+
4
+ def make_mock_response
5
+ m = mock()
6
+ m.stubs(:code).returns(200)
7
+ m.expects(:parsed_response).returns(['foo'])
8
+ m
9
+ end
10
+
11
+ def make_mock_query
12
+ m = mock()
13
+ m.expects(:build)
14
+ m.expects(:summarize_by).returns(m)
15
+ m
16
+ end
17
+
18
+ def expect_include_total(mock_query)
19
+ mock_query.expects(:include_total).with(true)
20
+ mock_query
21
+ end
22
+
23
+ describe 'raise_if_error' do
24
+ settings = {'server' => 'http://localhost:8080'}
25
+
26
+ it 'works with 4xx' do
27
+ response = mock()
28
+ response.stubs(:code).returns(400)
29
+
30
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should raise_error
31
+ end
32
+
33
+ it 'works with 5xx' do
34
+ response = mock()
35
+ response.stubs(:code).returns(500)
36
+
37
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should raise_error
38
+ end
39
+
40
+ it 'ignores 2xx' do
41
+ response = mock()
42
+ response.stubs(:code).returns(200)
43
+
44
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should_not raise_error
45
+ end
46
+
47
+ it 'ignores 3xx' do
48
+ response = mock()
49
+ response.stubs(:code).returns(300)
50
+
51
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should_not raise_error
52
+ end
53
+ end
54
+
55
+ describe 'SSL support' do
56
+ describe 'when http:// is specified' do
57
+ it 'does not use ssl' do
58
+ settings = {
59
+ 'server' => 'http://localhost:8080'
60
+ }
61
+
62
+ r = PuppetDB::Client.new(settings)
63
+ expect(r.use_ssl).to eq(false)
64
+ end
65
+ end
66
+
67
+ describe 'when https:// is specified' do
68
+ it 'uses ssl' do
69
+ settings = {
70
+ 'server' => 'https://localhost:8081',
71
+ 'pem' => {
72
+ 'cert' => 'foo',
73
+ 'key' => 'bar',
74
+ 'ca_file' => 'baz'
75
+ }
76
+ }
77
+
78
+ r = PuppetDB::Client.new(settings)
79
+ expect(r.use_ssl).to eq(true)
80
+ end
81
+
82
+ it 'does not tolerate lack of pem' do
83
+ settings = {
84
+ :server => 'https://localhost:8081'
85
+ }
86
+
87
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
88
+ end
89
+
90
+ it 'does not tolerate lack of key' do
91
+ settings = {
92
+ 'server' => 'https://localhost:8081',
93
+ 'pem' => {
94
+ 'cert' => 'foo',
95
+ 'ca_file' => 'bar'
96
+ }
97
+ }
98
+
99
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
100
+ end
101
+
102
+ it 'does not tolerate lack of cert' do
103
+ settings = {
104
+ 'server' => 'https://localhost:8081',
105
+ 'pem' => {
106
+ 'key' => 'foo',
107
+ 'ca_file' => 'bar'
108
+ }
109
+ }
110
+
111
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
112
+ end
113
+
114
+ it 'does not tolerate lack of ca_file' do
115
+ settings = {
116
+ 'server' => 'https://localhost:8081',
117
+ 'pem' => {
118
+ 'key' => 'foo',
119
+ 'cert' => 'bar'
120
+ }
121
+ }
122
+
123
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
124
+ end
125
+ end
126
+
127
+ describe 'when a protocol is missing from config file' do
128
+ it 'raises an exception' do
129
+ settings = {
130
+ 'server' => 'localhost:8080'
131
+ }
132
+
133
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
134
+ end
135
+ end
136
+ end
137
+
138
+ describe 'request' do
139
+ settings = {:server => 'http://localhost'}
140
+
141
+ it 'works with array instead of Query' do
142
+ client = PuppetDB::Client.new(settings)
143
+
144
+ mock_response = mock()
145
+ mock_response.expects(:code).returns(200)
146
+ mock_response.expects(:headers).returns({'X-Records' => 0})
147
+ mock_response.expects(:parsed_response).returns([])
148
+
149
+ PuppetDB::Client.expects(:get).returns(mock_response).at_least_once.with() do |path, opts|
150
+ opts[:query] == {'query' => '[1,2,3]'}
151
+ end
152
+ client.request('/foo', [1,2,3])
153
+ end
154
+
155
+ it 'processes options correctly' do
156
+ client = PuppetDB::Client.new(settings)
157
+
158
+ mock_response = mock()
159
+ mock_response.expects(:code).returns(200)
160
+ mock_response.expects(:headers).returns({'X-Records' => 0})
161
+ mock_response.expects(:parsed_response).returns([])
162
+
163
+ PuppetDB::Client.expects(:get).returns(mock_response).at_least_once.with() do |path, opts|
164
+ opts == {
165
+ :query => {
166
+ 'query' => '[1,2,3]',
167
+ 'limit' => 10,
168
+ 'counts-filter' => '[4,5,6]',
169
+ 'foo-bar' => 'foo'
170
+ }}
171
+ end
172
+
173
+ client.request('/foo', PuppetDB::Query[1,2,3], {
174
+ :limit => 10,
175
+ :counts_filter => [4,5,6],
176
+ :foo_bar => "foo"
177
+ })
178
+ end
179
+ end
@@ -0,0 +1,179 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+ require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'puppetdb')
3
+
4
+ def make_mock_response
5
+ m = mock()
6
+ m.stubs(:code).returns(200)
7
+ m.expects(:parsed_response).returns(['foo'])
8
+ m
9
+ end
10
+
11
+ def make_mock_query
12
+ m = mock()
13
+ m.expects(:build)
14
+ m.expects(:summarize_by).returns(m)
15
+ m
16
+ end
17
+
18
+ def expect_include_total(mock_query)
19
+ mock_query.expects(:include_total).with(true)
20
+ mock_query
21
+ end
22
+
23
+ describe 'raise_if_error' do
24
+ settings = {'server' => 'http://localhost:8080'}
25
+
26
+ it 'works with 4xx' do
27
+ response = mock()
28
+ response.stubs(:code).returns(400)
29
+
30
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should raise_error
31
+ end
32
+
33
+ it 'works with 5xx' do
34
+ response = mock()
35
+ response.stubs(:code).returns(500)
36
+
37
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should raise_error
38
+ end
39
+
40
+ it 'ignores 2xx' do
41
+ response = mock()
42
+ response.stubs(:code).returns(200)
43
+
44
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should_not raise_error
45
+ end
46
+
47
+ it 'ignores 3xx' do
48
+ response = mock()
49
+ response.stubs(:code).returns(300)
50
+
51
+ lambda { PuppetDB::Client.new(settings).raise_if_error(response) }.should_not raise_error
52
+ end
53
+ end
54
+
55
+ describe 'SSL support' do
56
+ describe 'when http:// is specified' do
57
+ it 'does not use ssl' do
58
+ settings = {
59
+ 'server' => 'http://localhost:8080'
60
+ }
61
+
62
+ r = PuppetDB::Client.new(settings)
63
+ expect(r.use_ssl).to eq(false)
64
+ end
65
+ end
66
+
67
+ describe 'when https:// is specified' do
68
+ it 'uses ssl' do
69
+ settings = {
70
+ 'server' => 'https://localhost:8081',
71
+ 'pem' => {
72
+ 'cert' => 'foo',
73
+ 'key' => 'bar',
74
+ 'ca_file' => 'baz'
75
+ }
76
+ }
77
+
78
+ r = PuppetDB::Client.new(settings)
79
+ expect(r.use_ssl).to eq(true)
80
+ end
81
+
82
+ it 'does not tolerate lack of pem' do
83
+ settings = {
84
+ :server => 'https://localhost:8081'
85
+ }
86
+
87
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
88
+ end
89
+
90
+ it 'does not tolerate lack of key' do
91
+ settings = {
92
+ 'server' => 'https://localhost:8081',
93
+ 'pem' => {
94
+ 'cert' => 'foo',
95
+ 'ca_file' => 'bar'
96
+ }
97
+ }
98
+
99
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
100
+ end
101
+
102
+ it 'does not tolerate lack of cert' do
103
+ settings = {
104
+ 'server' => 'https://localhost:8081',
105
+ 'pem' => {
106
+ 'key' => 'foo',
107
+ 'ca_file' => 'bar'
108
+ }
109
+ }
110
+
111
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
112
+ end
113
+
114
+ it 'does not tolerate lack of ca_file' do
115
+ settings = {
116
+ 'server' => 'https://localhost:8081',
117
+ 'pem' => {
118
+ 'key' => 'foo',
119
+ 'cert' => 'bar'
120
+ }
121
+ }
122
+
123
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
124
+ end
125
+ end
126
+
127
+ describe 'when a protocol is missing from config file' do
128
+ it 'raises an exception' do
129
+ settings = {
130
+ 'server' => 'localhost:8080'
131
+ }
132
+
133
+ lambda { PuppetDB::Client.new(settings) }.should raise_error
134
+ end
135
+ end
136
+ end
137
+
138
+ describe 'request' do
139
+ settings = {:server => 'http://localhost'}
140
+
141
+ it 'works with array instead of Query' do
142
+ client = PuppetDB::Client.new(settings)
143
+
144
+ mock_response = mock()
145
+ mock_response.expects(:code).returns(200)
146
+ mock_response.expects(:headers).returns({'X-Records' => 0})
147
+ mock_response.expects(:parsed_response).returns([])
148
+
149
+ PuppetDB::Client.expects(:get).returns(mock_response).at_least_once.with() do |path, opts|
150
+ opts[:query] == {'query' => '[1,2,3]'}
151
+ end
152
+ client.request('/foo', [1,2,3])
153
+ end
154
+
155
+ it 'processes options correctly' do
156
+ client = PuppetDB::Client.new(settings)
157
+
158
+ mock_response = mock()
159
+ mock_response.expects(:code).returns(200)
160
+ mock_response.expects(:headers).returns({'X-Records' => 0})
161
+ mock_response.expects(:parsed_response).returns([])
162
+
163
+ PuppetDB::Client.expects(:get).returns(mock_response).at_least_once.with() do |path, opts|
164
+ opts == {
165
+ :query => {
166
+ 'query' => '[1,2,3]',
167
+ 'limit' => 10,
168
+ 'counts-filter' => '[4,5,6]',
169
+ 'foo-bar' => 'foo'
170
+ }}
171
+ end
172
+
173
+ client.request('/foo', PuppetDB::Query[1,2,3], {
174
+ :limit => 10,
175
+ :counts_filter => [4,5,6],
176
+ :foo_bar => "foo"
177
+ })
178
+ end
179
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: puppetdb-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nathaniel Smith
9
+ - Lindsey Smith
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-11-07 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: httparty
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ description:
32
+ email: info@puppetlabs.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - lib/puppetdb.rb
42
+ - lib/puppetdb/client.rb
43
+ - lib/puppetdb/query.rb
44
+ - lib/puppetdb/response.rb
45
+ - spec/spec_helper.rb
46
+ - spec/unit/client_spec.rb
47
+ - spec/unit/query_spec.rb
48
+ homepage: https://github.com/puppetlabs/puppetdb-ruby
49
+ licenses:
50
+ - apache
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.23
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Simple Ruby client library for PuppetDB API
73
+ test_files: []