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.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/NOTICE.txt +17 -0
- data/README.md +22 -0
- data/puppet/lib/puppet/application/storeconfigs.rb +4 -0
- data/puppet/lib/puppet/face/node/deactivate.rb +37 -0
- data/puppet/lib/puppet/face/node/status.rb +80 -0
- data/puppet/lib/puppet/face/storeconfigs.rb +193 -0
- data/puppet/lib/puppet/indirector/catalog/puppetdb.rb +400 -0
- data/puppet/lib/puppet/indirector/facts/puppetdb.rb +152 -0
- data/puppet/lib/puppet/indirector/facts/puppetdb_apply.rb +25 -0
- data/puppet/lib/puppet/indirector/node/puppetdb.rb +19 -0
- data/puppet/lib/puppet/indirector/resource/puppetdb.rb +108 -0
- data/puppet/lib/puppet/reports/puppetdb.rb +188 -0
- data/puppet/lib/puppet/util/puppetdb.rb +108 -0
- data/puppet/lib/puppet/util/puppetdb/char_encoding.rb +316 -0
- data/puppet/lib/puppet/util/puppetdb/command.rb +116 -0
- data/puppet/lib/puppet/util/puppetdb/command_names.rb +8 -0
- data/puppet/lib/puppet/util/puppetdb/config.rb +148 -0
- data/puppet/lib/puppet/util/puppetdb/http.rb +121 -0
- data/puppet/spec/README.markdown +8 -0
- data/puppet/spec/spec.opts +6 -0
- data/puppet/spec/spec_helper.rb +38 -0
- data/puppet/spec/unit/face/node/deactivate_spec.rb +28 -0
- data/puppet/spec/unit/face/node/status_spec.rb +43 -0
- data/puppet/spec/unit/face/storeconfigs_spec.rb +199 -0
- data/puppet/spec/unit/indirector/catalog/puppetdb_spec.rb +703 -0
- data/puppet/spec/unit/indirector/facts/puppetdb_apply_spec.rb +27 -0
- data/puppet/spec/unit/indirector/facts/puppetdb_spec.rb +347 -0
- data/puppet/spec/unit/indirector/node/puppetdb_spec.rb +61 -0
- data/puppet/spec/unit/indirector/resource/puppetdb_spec.rb +199 -0
- data/puppet/spec/unit/reports/puppetdb_spec.rb +249 -0
- data/puppet/spec/unit/util/puppetdb/char_encoding_spec.rb +212 -0
- data/puppet/spec/unit/util/puppetdb/command_spec.rb +98 -0
- data/puppet/spec/unit/util/puppetdb/config_spec.rb +227 -0
- data/puppet/spec/unit/util/puppetdb/http_spec.rb +138 -0
- data/puppet/spec/unit/util/puppetdb_spec.rb +33 -0
- 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,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
|