ml-puppetdb-terminus 3.2.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/NOTICE.txt +17 -0
  4. data/README.md +22 -0
  5. data/puppet/lib/puppet/application/storeconfigs.rb +4 -0
  6. data/puppet/lib/puppet/face/node/deactivate.rb +37 -0
  7. data/puppet/lib/puppet/face/node/status.rb +80 -0
  8. data/puppet/lib/puppet/face/storeconfigs.rb +193 -0
  9. data/puppet/lib/puppet/indirector/catalog/puppetdb.rb +400 -0
  10. data/puppet/lib/puppet/indirector/facts/puppetdb.rb +152 -0
  11. data/puppet/lib/puppet/indirector/facts/puppetdb_apply.rb +25 -0
  12. data/puppet/lib/puppet/indirector/node/puppetdb.rb +19 -0
  13. data/puppet/lib/puppet/indirector/resource/puppetdb.rb +108 -0
  14. data/puppet/lib/puppet/reports/puppetdb.rb +188 -0
  15. data/puppet/lib/puppet/util/puppetdb.rb +108 -0
  16. data/puppet/lib/puppet/util/puppetdb/char_encoding.rb +316 -0
  17. data/puppet/lib/puppet/util/puppetdb/command.rb +116 -0
  18. data/puppet/lib/puppet/util/puppetdb/command_names.rb +8 -0
  19. data/puppet/lib/puppet/util/puppetdb/config.rb +148 -0
  20. data/puppet/lib/puppet/util/puppetdb/http.rb +121 -0
  21. data/puppet/spec/README.markdown +8 -0
  22. data/puppet/spec/spec.opts +6 -0
  23. data/puppet/spec/spec_helper.rb +38 -0
  24. data/puppet/spec/unit/face/node/deactivate_spec.rb +28 -0
  25. data/puppet/spec/unit/face/node/status_spec.rb +43 -0
  26. data/puppet/spec/unit/face/storeconfigs_spec.rb +199 -0
  27. data/puppet/spec/unit/indirector/catalog/puppetdb_spec.rb +703 -0
  28. data/puppet/spec/unit/indirector/facts/puppetdb_apply_spec.rb +27 -0
  29. data/puppet/spec/unit/indirector/facts/puppetdb_spec.rb +347 -0
  30. data/puppet/spec/unit/indirector/node/puppetdb_spec.rb +61 -0
  31. data/puppet/spec/unit/indirector/resource/puppetdb_spec.rb +199 -0
  32. data/puppet/spec/unit/reports/puppetdb_spec.rb +249 -0
  33. data/puppet/spec/unit/util/puppetdb/char_encoding_spec.rb +212 -0
  34. data/puppet/spec/unit/util/puppetdb/command_spec.rb +98 -0
  35. data/puppet/spec/unit/util/puppetdb/config_spec.rb +227 -0
  36. data/puppet/spec/unit/util/puppetdb/http_spec.rb +138 -0
  37. data/puppet/spec/unit/util/puppetdb_spec.rb +33 -0
  38. metadata +115 -0
@@ -0,0 +1,121 @@
1
+ require 'uri'
2
+ require 'puppet/network/http_pool'
3
+ require 'net/http'
4
+ require 'timeout'
5
+ require 'pp'
6
+
7
+ module Puppet::Util::Puppetdb
8
+ class Http
9
+
10
+ SERVER_URL_FAIL_MSG = "Failing over to the next PuppetDB url in the 'server_urls' list"
11
+
12
+ # Concat two url snippets, taking into account a trailing/leading slash to
13
+ # ensure a correct url is constructed
14
+ #
15
+ # @param snippet1 [String] first URL snippet
16
+ # @param snippet2 [String] second URL snippet
17
+ # @return [String] returns http response
18
+ # @api private
19
+ def self.concat_url_snippets(snippet1, snippet2)
20
+ if snippet1.end_with?('/') and snippet2.start_with?('/')
21
+ snippet1 + snippet2[1..-1]
22
+ elsif !snippet1.end_with?('/') and !snippet2.start_with?('/')
23
+ snippet1 + '/' + snippet2
24
+ else
25
+ snippet1 + snippet2
26
+ end
27
+ end
28
+
29
+ # Setup an http connection, provide a block that will do something with that http
30
+ # connection. The block should be a two argument block, accepting the connection (which
31
+ # you can call get or post on for example) and the properly constructed path, which
32
+ # will be the concatenated version of any url_prefix and the path passed in.
33
+ #
34
+ # @param path_suffix [String] path for the get/post of the http action
35
+ # @param http_callback [Proc] proc containing the code calling the action on the http connection
36
+ # @return [Response] returns http response
37
+ def self.action(path_suffix, &http_callback)
38
+
39
+ response = nil
40
+ config = Puppet::Util::Puppetdb.config
41
+ server_url_config = config.server_url_config?
42
+
43
+ for url in Puppet::Util::Puppetdb.config.server_urls
44
+ begin
45
+ route = concat_url_snippets(url.request_uri, path_suffix)
46
+ http = Puppet::Network::HttpPool.http_instance(url.host, url.port)
47
+ request_timeout = config.server_url_timeout
48
+
49
+ response = timeout(request_timeout) do
50
+ http_callback.call(http, route)
51
+ end
52
+
53
+ if response.is_a? Net::HTTPServerError
54
+ Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
55
+ response = nil
56
+ elsif response.is_a? Net::HTTPNotFound
57
+ if response.body && response.body.chars.first == "{"
58
+ # If it appears to be json, we've probably gotten an authentic 'not found' message.
59
+ Puppet.debug("HTTP 404 (probably normal) when connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
60
+ response = :notfound
61
+ else
62
+ # But we can also get 404s when conneting to a puppetdb that's still starting or due to misconfiguration.
63
+ Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{response.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
64
+ response = nil
65
+ end
66
+ else
67
+ break
68
+ end
69
+ rescue Timeout::Error => e
70
+ Puppet.warning("Request to #{url.host} on #{url.port} at route #{route} timed out after #{request_timeout} seconds. #{SERVER_URL_FAIL_MSG if server_url_config}")
71
+
72
+ rescue SocketError, OpenSSL::SSL::SSLError, SystemCallError, Net::ProtocolError, IOError, Net::HTTPNotFound => e
73
+ Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{e.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
74
+
75
+ rescue Puppet::Util::Puppetdb::InventorySearchError => e
76
+ Puppet.warning("Could not perform inventory search from PuppetDB at #{url.host}:#{url.port}: '#{e.message}' #{SERVER_URL_FAIL_MSG if server_url_config}")
77
+
78
+ rescue Puppet::Util::Puppetdb::CommandSubmissionError => e
79
+ error = "Failed to submit '#{e.context[:command]}' command for '#{e.context[:for_whom]}' to PuppetDB at #{url.host}:#{url.port}: '#{e.message}'."
80
+ if config.soft_write_failure
81
+ Puppet.err error
82
+ else
83
+ Puppet.warning(error + " #{SERVER_URL_FAIL_MSG if server_url_config}")
84
+ end
85
+ rescue Puppet::Util::Puppetdb::SoftWriteFailError => e
86
+ Puppet.warning("Failed to submit '#{e.context[:command]}' command for '#{e.context[:for_whom]}' to PuppetDB at #{url.host}:#{url.port}: '#{e.message}' #{SERVER_URL_FAIL_MSG if server_url_config}")
87
+ rescue Puppet::Error => e
88
+ if e.message =~ /did not match server certificate; expected one of/
89
+ Puppet.warning("Error connecting to #{url.host} on #{url.port} at route #{route}, error message received was '#{e.message}'. #{SERVER_URL_FAIL_MSG if server_url_config}")
90
+ else
91
+ raise
92
+ end
93
+ end
94
+ end
95
+
96
+ if response.nil? or response == :notfound
97
+ if server_url_config
98
+ server_url_strings = Puppet::Util::Puppetdb.config.server_urls.map {|url| url.to_s}.join(', ')
99
+ if response == :notfound
100
+ raise NotFoundError, "Failed to find '#{path_suffix}' on any of the following 'server_urls': #{server_url_strings}"
101
+ else
102
+ raise Puppet::Error, "Failed to execute '#{path_suffix}' on any of the following 'server_urls': #{server_url_strings}"
103
+ end
104
+ else
105
+ uri = Puppet::Util::Puppetdb.config.server_urls.first
106
+ if response == :notfound
107
+ raise NotFoundError, "Failed to find '#{path_suffix}' on server: '#{uri.host}' and port: '#{uri.port}'"
108
+ else
109
+ raise Puppet::Error, "Failed to execute '#{path_suffix}' on server: '#{uri.host}' and port: '#{uri.port}'"
110
+ end
111
+ end
112
+ end
113
+
114
+ response
115
+
116
+ end
117
+ end
118
+
119
+ class NotFoundError < Puppet::Error
120
+ end
121
+ end
@@ -0,0 +1,8 @@
1
+ NOTE
2
+ ====
3
+
4
+ This project's specs depend on puppet core, and thus they require the
5
+ puppetlabs_spec_helper project. For more information please see the
6
+ README in that project, which can be found here:
7
+
8
+ https://github.com/puppetlabs/puppetlabs_spec_helper
@@ -0,0 +1,6 @@
1
+ --format
2
+ s
3
+ --colour
4
+ --loadby
5
+ mtime
6
+ --backtrace
@@ -0,0 +1,38 @@
1
+ dir = File.expand_path(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift File.join(dir, "../lib")
3
+ # Maybe puppetlabs_spec_helper is in a directory next to puppetdb. If not, we
4
+ # don't fail any worse than we already would.
5
+ $LOAD_PATH.push File.join(dir, "../../../puppetlabs_spec_helper")
6
+
7
+ require 'rspec'
8
+ require 'rspec/expectations'
9
+ require 'puppetlabs_spec_helper/puppet_spec_helper'
10
+ require 'tmpdir'
11
+ require 'fileutils'
12
+ require 'puppet'
13
+ require 'puppet/util/puppetdb'
14
+ require 'puppet/util/log'
15
+
16
+ def create_environmentdir(environment)
17
+ if not Puppet::Util::Puppetdb.puppet3compat?
18
+ envdir = File.join(Puppet[:environmentpath], environment)
19
+ if not Dir.exists?(envdir)
20
+ Dir.mkdir(envdir)
21
+ end
22
+ end
23
+ end
24
+
25
+ RSpec.configure do |config|
26
+
27
+ config.before :each do
28
+ @logs = []
29
+ Puppet::Util::Log.level = :info
30
+ Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs))
31
+
32
+ def test_logs
33
+ @logs.map(&:message)
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'spec_helper'
4
+ require 'puppet/face'
5
+ require 'puppet/indirector/node/puppetdb'
6
+
7
+ describe "node face: deactivate" do
8
+ let(:subject) { Puppet::Face[:node, :current] }
9
+
10
+ it "should fail if no node is given" do
11
+ expect { subject.deactivate }.to raise_error ArgumentError, /provide at least one node/
12
+ end
13
+
14
+ it "should deactivate each node using the puppetdb terminus" do
15
+ nodes = ['a', 'b', 'c']
16
+ nodes.each do |node|
17
+ Puppet::Node::Puppetdb.any_instance.expects(:destroy).with do |request|
18
+ request.key == node
19
+ end.returns('uuid' => "uuid_#{node}")
20
+ end
21
+
22
+ subject.deactivate(*nodes).should == {
23
+ 'a' => 'uuid_a',
24
+ 'b' => 'uuid_b',
25
+ 'c' => 'uuid_c',
26
+ }
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'spec_helper'
4
+ require 'puppet'
5
+ require 'puppet/face'
6
+ require 'puppet/network/http_pool'
7
+
8
+ describe "node face: status" do
9
+ let(:subject) { Puppet::Face[:node, :current] }
10
+ let(:headers) do
11
+ {
12
+ "Accept" => "application/json",
13
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
14
+ }
15
+ end
16
+
17
+ it "should fail if no node is given" do
18
+ expect { subject.status }.to raise_error ArgumentError, /provide at least one node/
19
+ end
20
+
21
+ it "should fetch the status of each node" do
22
+ http = stub 'http'
23
+ Puppet::Network::HttpPool.stubs(:http_instance).returns(http)
24
+
25
+ nodes = %w[a b c d e]
26
+ nodes.each do |node|
27
+ http.expects(:get).with("/pdb/query/v4/nodes/#{node}", headers)
28
+ end
29
+
30
+ subject.status(*nodes)
31
+ end
32
+
33
+ it "should CGI escape the node names" do
34
+ http = stub 'http'
35
+ Puppet::Network::HttpPool.stubs(:http_instance).returns(http)
36
+
37
+ node = "foo/+*&bar"
38
+
39
+ http.expects(:get).with("/pdb/query/v4/nodes/foo%2F%2B%2A%26bar", headers)
40
+
41
+ subject.status(node)
42
+ end
43
+ end
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'puppet/util/puppetdb'
4
+
5
+ if Puppet::Util::Puppetdb.puppet3compat?
6
+ require 'spec_helper'
7
+ require 'puppet/face/storeconfigs'
8
+ require 'json'
9
+ require 'puppet/util/feature'
10
+ require 'puppet/util/puppetdb'
11
+
12
+ describe Puppet::Face[:storeconfigs, '0.0.1'], :if => (Puppet.features.rails? && Puppet.features.sqlite?) do
13
+ def setup_scratch_database
14
+ Puppet::Rails.stubs(:database_arguments).returns(
15
+ :adapter => 'sqlite3',
16
+ :log_level => Puppet[:rails_loglevel],
17
+ :database => ':memory:'
18
+ )
19
+ Puppet[:railslog] = '/dev/null'
20
+ Puppet::Rails.init
21
+ end
22
+
23
+ before :all do
24
+ # We have to have this block to require this file, so they get loaded on
25
+ # platforms where we are going to run the tests, but not on Ruby 1.8.5.
26
+ # Unfortunately, rspec will evaluate the describe block (but not the before
27
+ # block or tests) even if the conditions fail. The lack of a sqlite3 gem
28
+ # for Ruby 1.8.5 ensures that the condition will always be false on Ruby
29
+ # 1.8.5, so at this point it's safe to require this.
30
+ require 'puppet/indirector/catalog/active_record'
31
+ end
32
+
33
+ before :each do
34
+ setup_scratch_database
35
+ Puppet[:storeconfigs] = true
36
+ Puppet[:storeconfigs_backend] = :active_record
37
+ end
38
+
39
+ describe "export action" do
40
+ after :each do
41
+ FileUtils.rm_rf(@path)
42
+ end
43
+
44
+ before :each do
45
+ tempfile = Tempfile.new('export')
46
+ @path = tempfile.path
47
+ tempfile.close!
48
+
49
+ Dir.mkdir(@path)
50
+
51
+ subject.stubs(:destination_file).returns File.join(@path, 'storeconfigs-test.tar')
52
+ end
53
+
54
+ # Turn the filename of a gzipped tar into a hash from filename to content.
55
+ def tgz_to_hash(filename)
56
+ # List the files in the archive, ignoring directories (whose names end
57
+ # with /), and stripping the leading puppetdb-bak.
58
+ files = `tar tf #{filename}`.lines.map(&:chomp).reject { |fname| fname[-1,1] == '/'}.map {|fname| fname.sub('puppetdb-bak/', '') }
59
+
60
+ # Get the content of the files, one per line. Thank goodness they're a
61
+ # single line each.
62
+ content = `tar xf #{filename} -O`.lines.to_a
63
+
64
+ # Build a hash from filename to content. Ruby 1.8.5 doesn't like
65
+ # Hash[array_of_pairs], so we have to jump through hoops by flattening
66
+ # and splatting this list.
67
+ Hash[*files.zip(content).flatten]
68
+ end
69
+
70
+ describe "with nodes present" do
71
+ def notify(title, exported=false)
72
+ Puppet::Resource.new(:notify, title, :parameters => {:message => title}, :exported => exported)
73
+ end
74
+
75
+ def user(name)
76
+ Puppet::Resource.new(:user, name,
77
+ :parameters => {:groups => ['foo', 'bar', 'baz'],
78
+ :profiles => ['stuff', 'here'] #<-- Uses an ordered list
79
+ }, :exported => true)
80
+ end
81
+
82
+ def save_catalog(catalog)
83
+ request = Puppet::Resource::Catalog.indirection.request(:save, catalog.name, catalog)
84
+ Puppet::Resource::Catalog::ActiveRecord.new.save(request)
85
+ end
86
+
87
+ before :each do
88
+ catalog = Puppet::Resource::Catalog.new('foo')
89
+
90
+ catalog.add_resource notify('not exported')
91
+ catalog.add_resource notify('exported', true)
92
+ catalog.add_resource user('someuser')
93
+ save_catalog(catalog)
94
+ end
95
+
96
+ it "should have the right structure" do
97
+ filename = subject.export
98
+
99
+ results = tgz_to_hash(filename)
100
+
101
+ results.keys.should =~ ['export-metadata.json', 'catalogs/foo.json']
102
+
103
+ metadata = JSON.load(results['export-metadata.json'])
104
+
105
+ metadata.keys.should =~ ['timestamp', 'command_versions']
106
+ metadata['command_versions'].should == {'replace_catalog' => 6}
107
+
108
+ catalog = JSON.load(results['catalogs/foo.json'])
109
+
110
+ catalog.keys.should =~ ['metadata', 'environment', 'certname', 'version', 'edges', 'resources', 'timestamp', 'producer_timestamp']
111
+
112
+ catalog['metadata'].should == {'api_version' => 1}
113
+
114
+ catalog['certname'].should == 'foo'
115
+ catalog['edges'].to_set.should == [{
116
+ 'source' => {'type' => 'Stage', 'title' => 'main'},
117
+ 'target' => {'type' => 'Notify', 'title' => 'exported'},
118
+ 'relationship' => 'contains'},
119
+ {"source"=>{"type"=>"Stage", "title"=>"main"},
120
+ "target"=>{"type"=>"User", "title"=>"someuser"},
121
+ "relationship"=>"contains"}].to_set
122
+
123
+ catalog['resources'].should include({
124
+ 'type' => 'Stage',
125
+ 'title' => 'main',
126
+ 'exported' => false,
127
+ 'tags' => ['stage', 'main'],
128
+ 'parameters' => {},
129
+ })
130
+
131
+ catalog['resources'].should include({
132
+ 'type' => 'Notify',
133
+ 'title' => 'exported',
134
+ 'exported' => true,
135
+ 'tags' => ['exported', 'notify'],
136
+ 'parameters' => {
137
+ 'message' => 'exported',
138
+ },
139
+ })
140
+
141
+ catalog['resources'].should include({
142
+ 'type' => 'User',
143
+ 'title' => 'someuser',
144
+ 'exported' => true,
145
+ 'tags' => ['someuser', 'user'],
146
+ 'parameters' => {
147
+ 'groups' => ['foo', 'bar', 'baz'],
148
+ 'profiles' => ['stuff', 'here']
149
+ },
150
+ })
151
+ end
152
+
153
+ it "should only include exported resources and Stage[main]" do
154
+ filename = subject.export
155
+
156
+ results = tgz_to_hash(filename)
157
+
158
+ results.keys.should =~ ['export-metadata.json', 'catalogs/foo.json']
159
+
160
+ catalog = JSON.load(results['catalogs/foo.json'])
161
+
162
+ catalog['certname'].should == 'foo'
163
+
164
+ catalog['edges'].map do |edge|
165
+ [edge['source']['type'], edge['source']['title'], edge['relationship'], edge['target']['type'], edge['target']['title']]
166
+ end.to_set.should == [['Stage', 'main', 'contains', 'Notify', 'exported'],
167
+ ['Stage', 'main', 'contains', 'User', 'someuser']].to_set
168
+
169
+ catalog['resources'].map { |resource| [resource['type'], resource['title']] }.to_set.should == [['Notify', 'exported'], ["User", "someuser"], ['Stage', 'main']].to_set
170
+
171
+ notify = catalog['resources'].find {|resource| resource['type'] == 'Notify'}
172
+
173
+ notify['exported'].should == true
174
+ end
175
+
176
+ it "should exclude nodes with no exported resources" do
177
+ catalog = Puppet::Resource::Catalog.new('bar')
178
+
179
+ catalog.add_resource notify('also not exported')
180
+
181
+ save_catalog(catalog)
182
+
183
+ filename = subject.export
184
+
185
+ results = tgz_to_hash(filename)
186
+
187
+ results.keys.should =~ ['export-metadata.json', 'catalogs/foo.json']
188
+ end
189
+ end
190
+
191
+ it "should do nothing if there are no nodes" do
192
+ filename = subject.export
193
+
194
+ results = tgz_to_hash(filename)
195
+ results.keys.should == ['export-metadata.json']
196
+ end
197
+ end
198
+ end
199
+ end