puppetdb-ruby 0.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.
@@ -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: []