soloist 1.0.0.pre → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/soloist.gemspec CHANGED
@@ -23,10 +23,13 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency "librarian"
24
24
  s.add_dependency "thor"
25
25
  s.add_dependency "hashie"
26
+ s.add_dependency "net-ssh"
27
+ s.add_dependency "awesome_print"
26
28
 
27
29
  s.add_development_dependency "rspec"
28
30
  s.add_development_dependency "guard-rspec"
29
31
  s.add_development_dependency "guard-bundler"
32
+ s.add_development_dependency "guard-shell"
30
33
  s.add_development_dependency "rb-fsevent"
31
34
  s.add_development_dependency "terminal-notifier-guard"
32
35
  s.add_development_dependency "gem-release"
@@ -0,0 +1,8 @@
1
+ def make_story_channel(&block)
2
+ story do |session|
3
+ channel = session.opens_channel
4
+ block.call(channel)
5
+ channel.gets_close
6
+ channel.sends_close
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ require "net/ssh/test"
2
+
3
+ class Net::SSH::Test::Packet
4
+ alias :original_types :types
5
+ def types
6
+ @types ||= case @type
7
+ when CHANNEL_EXTENDED_DATA then [:long, :long, :string]
8
+ else original_types
9
+ end
10
+ end
11
+ end
12
+
13
+ class Net::SSH::Test::Script
14
+ def gets_channel_extended_data(channel, data)
15
+ events << Net::SSH::Test::RemotePacket.new(:channel_extended_data, channel.local_id, 1, data)
16
+ end
17
+ end
18
+
19
+ class Net::SSH::Test::Channel
20
+ def gets_extended_data(data)
21
+ script.gets_channel_extended_data(self, data)
22
+ end
23
+ end
24
+
25
+ class Net::SSH::Test::Kex
26
+ def exchange_keys
27
+ result = Net::SSH::Buffer.from(:byte, NEWKEYS)
28
+ @connection.send_message(result)
29
+
30
+ buffer = @connection.next_message
31
+ raise Net::SSH::Exception, "expected NEWKEYS" unless buffer.type == NEWKEYS
32
+
33
+ { :session_id => "abc-xyz",
34
+ :server_key => OpenSSL::PKey::RSA.new(512),
35
+ :shared_secret => OpenSSL::BN.new("1234567890", 10),
36
+ :hashing_algorithm => OpenSSL::Digest::SHA1 }
37
+ end
38
+ end
@@ -2,12 +2,27 @@ require "spec_helper"
2
2
 
3
3
  describe Soloist::CLI do
4
4
  let(:cli) { Soloist::CLI.new }
5
- let(:base_path) { Dir.mktmpdir }
5
+ let(:base_path) { RSpec.configuration.tempdir }
6
6
  let(:soloistrc_path) { File.expand_path("soloistrc", base_path) }
7
7
 
8
- before { FileUtils.mkdir_p(base_path) }
8
+ before do
9
+ FileUtils.mkdir_p(base_path)
10
+ Soloist::Config.any_instance.stub(:exec)
11
+ end
9
12
 
10
13
  describe "#chef" do
14
+ it "receives the outside environment" do
15
+ FileUtils.touch(soloistrc_path)
16
+ Dir.chdir(base_path) do
17
+ ENV["AUTREYISM"] = "pathological-yodeling"
18
+ cli.soloist_config.should_receive(:exec) do |chef_solo|
19
+ `#{chef_solo}`.chomp.should == "pathological-yodeling"
20
+ end
21
+ cli.soloist_config.stub(:chef_solo).and_return('echo $AUTREYISM')
22
+ cli.chef
23
+ end
24
+ end
25
+
11
26
  context "when the soloistrc file does not exist" do
12
27
  it "raises an error" do
13
28
  expect do
@@ -26,55 +41,84 @@ describe Soloist::CLI do
26
41
  File.open(soloistrc_path, "w") do |file|
27
42
  file.write(YAML.dump("recipes" => ["stinky::feet"]))
28
43
  end
44
+ cli.soloist_config = nil
45
+ Dir.chdir(base_path) { cli.soloist_config.stub(:exec) }
29
46
  end
30
47
 
31
48
  it "runs the proper recipes" do
32
- cli.stub(:exec)
33
- Dir.chdir(base_path) { cli.chef }
34
- cli.config.royal_crown.recipes.should =~ ["stinky::feet"]
49
+ cli.chef
50
+ cli.soloist_config.royal_crown.recipes.should =~ ["stinky::feet"]
51
+ end
52
+
53
+ context "when a soloistrc_local file exists" do
54
+ let(:soloistrc_local_path) { File.expand_path("soloistrc_local", base_path) }
55
+
56
+ before do
57
+ File.open(soloistrc_local_path, "w") do |file|
58
+ file.write(YAML.dump("recipes" => ["stinky::socks"]))
59
+ end
60
+ cli.soloist_config = nil
61
+ Dir.chdir(base_path) { cli.soloist_config.stub(:exec) }
62
+ end
63
+
64
+ it "installs the proper recipes" do
65
+ cli.chef
66
+ cli.soloist_config.royal_crown.recipes.should =~ ["stinky::feet", "stinky::socks"]
67
+ end
35
68
  end
36
69
 
37
70
  context "when the Cheffile does not exist" do
38
71
  it "runs chef" do
39
- cli.should_receive(:exec)
40
- Dir.chdir(base_path) { cli.chef }
72
+ cli.soloist_config.should_receive(:exec)
73
+ cli.chef
41
74
  end
42
75
 
43
76
  it "does not run librarian" do
44
- cli.stub(:exec)
45
77
  Librarian::Chef::Cli.should_not_receive(:with_environment)
46
- Dir.chdir(base_path) { cli.chef }
78
+ cli.chef
47
79
  end
48
80
  end
49
81
 
50
82
  context "when the Cheffile exists" do
51
83
  let(:cli_instance) { double(:cli_instance) }
52
84
 
53
- before do
54
- FileUtils.touch(File.expand_path("Cheffile", base_path))
55
- cli.stub(:exec)
56
- end
85
+ before { FileUtils.touch(File.expand_path("Cheffile", base_path)) }
57
86
 
58
87
  it "runs librarian" do
59
88
  Librarian::Chef::Cli.should_receive(:with_environment).and_yield
60
89
  Librarian::Chef::Cli.should_receive(:new).and_return(cli_instance)
61
90
  cli_instance.should_receive(:install)
62
- Dir.chdir(base_path) { cli.chef }
91
+ cli.chef
63
92
  end
64
93
 
65
- it "runs chef" do
66
- cli.should_receive(:exec)
67
- Dir.chdir(base_path) { cli.chef }
94
+ context "when the user is not root" do
95
+ it "creates the cache path using sudo" do
96
+ cli.soloist_config.should_receive(:exec) do |command|
97
+ command.should =~ /^sudo -E/
98
+ end
99
+ cli.chef
100
+ end
101
+ end
102
+
103
+ context "when the user is root" do
104
+ before { Process.stub(:uid => 0) }
105
+
106
+ it "creates the cache path" do
107
+ cli.soloist_config.should_receive(:exec) do |command|
108
+ command.should_not =~ /^sudo -E/
109
+ end
110
+ cli.chef
111
+ end
68
112
  end
69
113
  end
70
114
  end
71
115
  end
72
116
 
73
- describe "#DO_IT_LIVE" do
117
+ describe "#run_recipe" do
74
118
  context "when the soloistrc does not exist" do
75
119
  it "raises an error" do
76
120
  expect do
77
- Dir.chdir(base_path) { cli.DO_IT_LIVE("pineapple::wut") }
121
+ Dir.chdir(base_path) { cli.run_recipe("pineapple::wut") }
78
122
  end.to raise_error(Soloist::NotFound)
79
123
  end
80
124
  end
@@ -89,45 +133,22 @@ describe Soloist::CLI do
89
133
  it "sets a recipe to run" do
90
134
  Dir.chdir(base_path) do
91
135
  cli.should_receive(:chef)
92
- cli.DO_IT_LIVE("angst::teenage", "ennui::default")
93
- cli.config.royal_crown.recipes.should =~ ["angst::teenage", "ennui::default"]
136
+ cli.run_recipe("angst::teenage", "ennui::default")
137
+ cli.soloist_config.royal_crown.recipes.should =~ ["angst::teenage", "ennui::default"]
94
138
  end
95
139
  end
96
140
  end
97
141
  end
98
142
 
99
- describe "#ensure_chef_cache_path" do
100
- context "when the cache path does not exist" do
101
- before { File.stub(:directory? => false) }
102
-
103
- it "creates the cache path" do
104
- cli.should_receive(:system).with("sudo mkdir -p /var/chef/cache")
105
- cli.ensure_chef_cache_path
106
- end
107
- end
108
-
109
- context "when the cache path exists" do
110
- before { File.stub(:directory? => true) }
111
-
112
- it "does not create the cache path" do
113
- cli.should_not_receive(:system)
114
- cli.ensure_chef_cache_path
115
- end
116
- end
117
- end
143
+ describe "#config" do
144
+ let(:royal_crown) { Soloist::RoyalCrown.new(:node_attributes => {"a" => "b"}) }
145
+ let(:config) { Soloist::Config.new(royal_crown) }
118
146
 
119
- describe "#chef_solo" do
120
- before do
121
- ENV["AUTREYISM"] = "pathological-yodeling"
122
- FileUtils.touch(File.expand_path("soloistrc", base_path))
123
- end
147
+ before { cli.stub(:soloist_config => config) }
124
148
 
125
- it "receives the outside environment" do
126
- cli.stub(:chef_solo).and_return('echo $AUTREYISM')
127
- cli.should_receive(:exec) do |chef_solo|
128
- `#{chef_solo}`.chomp.should == "pathological-yodeling"
129
- end
130
- Dir.chdir(base_path) { cli.chef }
149
+ it "prints the hash render of the RoyalCrown" do
150
+ Kernel.should_receive(:ap).with({"recipes"=>[], "a" => "b"})
151
+ cli.config
131
152
  end
132
153
  end
133
154
  end
@@ -1,107 +1,196 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Soloist::Config do
4
- let(:soloist_rc) { Soloist::RoyalCrown.new(:path => "/tmp/soloist/soloistrc") }
4
+ let(:soloist_rc_path) { File.expand_path("soloistrc", RSpec.configuration.tempdir) }
5
+ let(:soloist_rc) { Soloist::RoyalCrown.new(:path => soloist_rc_path) }
5
6
  let(:config) { Soloist::Config.new(soloist_rc) }
7
+ let(:cookbook_path) { File.expand_path("cookbooks", RSpec.configuration.tempdir) }
8
+ let(:nested_cookbook_path) { File.expand_path("whoa/cookbooks", RSpec.configuration.tempdir) }
6
9
 
7
10
  describe "#as_solo_rb" do
8
- context "without extra cookbook paths" do
9
- it "can generate solo.rb" do
10
- config.as_solo_rb.should == 'cookbook_path ["/tmp/soloist/cookbooks"]'
11
- end
11
+ subject { config.as_solo_rb }
12
+
13
+ it { should include 'file_cache_path "/var/chef/cache"' }
14
+ it { should include %(json_attribs "#{config.node_json_path}") }
15
+ end
16
+
17
+ describe "#cookbook_paths" do
18
+ subject { config.cookbook_paths }
19
+
20
+ context "when the default cookbook path does not exist" do
21
+ it { should have(0).paths }
12
22
  end
13
23
 
14
- context "with a cookbook path" do
15
- before { soloist_rc.cookbook_paths = ["/opt/holla/at/yo/soloist"] }
24
+ context "when the default cookbook path exists" do
25
+ before { FileUtils.mkdir_p(cookbook_path) }
26
+
27
+ it { should have(1).path }
28
+ it { should =~ [cookbook_path] }
29
+
30
+ context "when the default cookbook path is specified" do
31
+ before { soloist_rc.cookbook_paths = [cookbook_path] }
16
32
 
17
- it "can have multiple cookbook paths" do
18
- config.as_solo_rb.should == 'cookbook_path ["/tmp/soloist/cookbooks", "/opt/holla/at/yo/soloist"]'
33
+ it { should have(1).path }
34
+ it { should =~ [cookbook_path] }
19
35
  end
20
36
 
21
- it "removes duplicate cookbook paths" do
22
- expect do
23
- soloist_rc.cookbook_paths << "/opt/holla/at/yo/soloist"
24
- end.not_to change { config.as_solo_rb }
37
+ context "with a specified cookbook path" do
38
+ before { soloist_rc.cookbook_paths = [nested_cookbook_path] }
39
+
40
+ context "when the specified path exists" do
41
+ before { FileUtils.mkdir_p(nested_cookbook_path) }
42
+
43
+ it { should have(2).paths }
44
+ it { should =~ [cookbook_path, nested_cookbook_path] }
45
+
46
+ context "with duplicate cookbook paths" do
47
+ before { soloist_rc.cookbook_paths = [nested_cookbook_path, nested_cookbook_path] }
48
+
49
+ it { should have(2).paths }
50
+ it { should =~ [cookbook_path, nested_cookbook_path] }
51
+ end
52
+ end
53
+
54
+ context "when the specified path does not exist" do
55
+ it { should have(1).path }
56
+ it { should =~ [cookbook_path] }
57
+ end
25
58
  end
26
59
  end
27
60
 
28
61
  context "with relative paths" do
29
- let(:pwd) { File.expand_path(".") }
30
-
31
- before { soloist_rc.cookbook_paths << "./meth/cookbooks" }
32
-
33
- it "can have multiple cookbook paths" do
34
- config.as_solo_rb.should == 'cookbook_path ["/tmp/soloist/cookbooks", "/tmp/soloist/meth/cookbooks"]'
62
+ before do
63
+ soloist_rc.cookbook_paths = ["./whoa/cookbooks"]
64
+ FileUtils.mkdir_p(nested_cookbook_path)
35
65
  end
66
+
67
+ it { should have(1).path }
68
+ it { should =~ [nested_cookbook_path] }
36
69
  end
37
70
 
38
71
  context "with unixisms in the cookbook path" do
39
72
  let(:home) { File.expand_path("~") }
40
73
 
41
- before { soloist_rc.cookbook_paths << "~/yo/homes" }
74
+ before { soloist_rc.cookbook_paths = ["~"] }
42
75
 
43
- it "expands paths" do
44
- config.as_solo_rb.should include "#{home}/yo/homes"
45
- end
76
+ it { should have(1).path }
77
+ it { should =~ [home] }
46
78
  end
47
79
  end
48
80
 
49
- describe "#as_json" do
50
- context "with recipes" do
51
- before { soloist_rc.recipes = ["waffles"] }
52
-
53
- it "can generate json" do
54
- config.as_json["recipes"].should include "waffles"
55
- end
81
+ describe "#as_node_json" do
82
+ let(:soloist_rc) do
83
+ Soloist::RoyalCrown.new(
84
+ :path => soloist_rc_path,
85
+ :recipes => ["waffles"],
86
+ :node_attributes => { "gargling" => "cool", "birds" => {"nested" => "cheep"} }
87
+ )
56
88
  end
57
- end
58
89
 
59
- describe "#compiled_rc" do
60
- let(:switch) { {"OX_TONGUES" => {"FINE" => {"recipes" => ["hobo_fist"]}}} }
90
+ describe "node_attributes" do
91
+ subject { config.as_node_json }
61
92
 
62
- before do
63
- soloist_rc.env_variable_switches = switch
93
+ it { should include "gargling" => "cool" }
94
+ it { should include "birds" => { "nested" => "cheep" } }
64
95
  end
65
96
 
66
- context "when a switch is active" do
67
- before { ENV.stub(:[]).and_return("FINE") }
97
+ describe "recipes" do
98
+ subject { config.as_node_json["recipes"] }
68
99
 
69
- it "merges the environment variable switch" do
70
- config.compiled_rc.recipes.should include "hobo_fist"
71
- end
100
+ it { should have(1).recipe }
101
+ it { should =~ ["waffles"] }
72
102
  end
103
+ end
73
104
 
74
- context "when a switch is inactive" do
75
- before { ENV.stub(:[]).and_return("WHAT_NO_EW") }
105
+ describe "#compiled" do
106
+ let(:nested) { {} }
107
+ let(:switch) do
108
+ {
109
+ "TONGUES" => {
110
+ "FINE" => {
111
+ "recipes" => ["hobo_fist"],
112
+ "env_variable_switches" => nested
113
+ }
114
+ }
115
+ }
116
+ end
117
+
118
+ before { config.royal_crown.env_variable_switches = switch }
119
+
120
+ context "when the switch is inactive" do
121
+ before { ENV.stub(:[]).and_return("LOLWUT") }
76
122
 
77
- it "outputs an empty list" do
78
- config.compiled_rc.recipes.should be_empty
123
+ it "does not merge the attribute" do
124
+ config.compiled["recipes"].should be_empty
79
125
  end
80
126
  end
81
127
 
82
- context "when switches are nested" do
83
- let(:inner) { {"GOAT" => {"TASTY" => {"node_attributes" => {"bbq" => "satan"}}}} }
84
- let(:outer) { {"recipes" => ["stinkditch"], "env_variable_switches" => inner} }
128
+ context "when a switch is active" do
129
+ before { ENV.stub(:[]).and_return("FINE") }
85
130
 
86
- before { ENV.stub(:[]).and_return("TASTY") }
131
+ it "merges the attributes" do
132
+ config.compiled.recipes.should =~ ["hobo_fist"]
133
+ end
87
134
 
88
- context "when the inner switch is active" do
89
- let(:switch) { {"HORSE" => {"TASTY" => outer }} }
135
+ context "when an inactive switch is nested" do
136
+ let(:nested) { {"BEANS" => {"EW" => {"recipes" => ["slammin"]}}} }
90
137
 
91
- it "evalutes all switches" do
92
- config.compiled_rc.node_attributes.bbq.should == "satan"
93
- config.compiled_rc.recipes.should == ["stinkditch"]
138
+ it "does not merge the attributes" do
139
+ config.compiled.recipes.should =~ ["hobo_fist"]
94
140
  end
95
141
  end
96
142
 
97
- context "when the outer switch is inactive" do
98
- let(:switch) { {"HORSE" => {"GROSS" => outer }} }
143
+ context "when an active switch is nested" do
144
+ let(:nested) { {"BEANS" => {"FINE" => {"recipes" => ["slammin"]}}} }
99
145
 
100
- it "does not evaluate deeper" do
101
- config.compiled_rc.recipes.should be_empty
102
- config.compiled_rc.node_attributes.should be_empty
146
+ it "merges the attributes" do
147
+ config.compiled.recipes.should =~ ["slammin"]
103
148
  end
104
149
  end
105
150
  end
106
151
  end
152
+
153
+ describe "#merge!" do
154
+ let(:soloist_rc) { Soloist::RoyalCrown.new(:recipes => ["guts"], :node_attributes => {:reliable => "maybe"}) }
155
+ let(:other_rc) { Soloist::RoyalCrown.new(:recipes => ["chum"], :node_attributes => {:tasty => "maybe"}) }
156
+ let(:other_config) { Soloist::Config.new(other_rc) }
157
+
158
+ it "merges another config into the current one" do
159
+ config.merge!(other_config)
160
+ config.royal_crown.recipes.should =~ ["guts", "chum"]
161
+ config.royal_crown.node_attributes.keys.should =~ [:reliable, :tasty]
162
+ end
163
+
164
+ it "does not trample the other config" do
165
+ config.merge!(other_config)
166
+ other_config.royal_crown.recipes.should =~ ["chum"]
167
+ other_config.royal_crown.node_attributes.should == {:tasty => "maybe"}
168
+ end
169
+ end
170
+
171
+ describe "#log_level" do
172
+ subject { config.log_level }
173
+
174
+ context "when LOG_LEVEL is not set" do
175
+ it { should == "info" }
176
+ end
177
+
178
+ context "when LOG_LEVEL is set" do
179
+ before { ENV.stub(:[] => "BEANS") }
180
+ it { should == "BEANS" }
181
+ end
182
+ end
183
+
184
+ describe "#debug?" do
185
+ subject { config.debug? }
186
+
187
+ context "when log_level is not debug" do
188
+ it { should_not be }
189
+ end
190
+
191
+ context "when log_level is debug" do
192
+ before { config.stub(:log_level => "debug") }
193
+ it { should be }
194
+ end
195
+ end
107
196
  end
@@ -0,0 +1,84 @@
1
+ require "spec_helper"
2
+
3
+ describe Soloist::RemoteConfig do
4
+ let(:royal_crown_path) { File.expand_path("soloistrc", RSpec.configuration.tempdir) }
5
+ let(:royal_crown) { Soloist::RoyalCrown.new(:path => royal_crown_path) }
6
+ let(:remote) { Soloist::Remote.new("user", "host", "key") }
7
+ let(:remote_config) { Soloist::RemoteConfig.new(royal_crown, remote) }
8
+
9
+ before { remote.stub(:backtick => "", :system => 0) }
10
+
11
+ def commands_for(method)
12
+ [].tap do |commands|
13
+ remote.stub(:system) { |c| commands << c; 0 }
14
+ remote.stub(:backtick) { |c| commands << c; "" }
15
+ remote_config.send(method)
16
+ end
17
+ end
18
+
19
+ describe "#run_chef" do
20
+ it "runs chef" do
21
+ commands_for(:run_chef).last.should include "chef-solo"
22
+ end
23
+ end
24
+
25
+ describe "#solo_rb_path" do
26
+ it "sets the path to /etc/chef/solo.rb" do
27
+ remote_config.solo_rb_path.should == "/etc/chef/solo.rb"
28
+ end
29
+
30
+ it "sets up solo.rb remotely" do
31
+ commands_for(:solo_rb_path).last.should =~ /sudo -E tee \/etc\/chef\/solo\.rb$/
32
+ end
33
+ end
34
+
35
+ describe "#node_json_path" do
36
+ it "sets the path" do
37
+ remote_config.node_json_path.should == "/etc/chef/node.json"
38
+ end
39
+
40
+ it "sets up node.json remotely" do
41
+ commands_for(:node_json_path).last.should =~ /sudo -E tee \/etc\/chef\/node\.json$/
42
+ end
43
+ end
44
+
45
+ describe "#chef_config_path" do
46
+ it "sets the path" do
47
+ remote_config.chef_config_path.should == "/etc/chef"
48
+ end
49
+
50
+ it "creates the path remotely" do
51
+ commands_for(:chef_config_path).tap do |commands|
52
+ commands.should have(1).command
53
+ commands.first.should =~ /mkdir .*? -p \/etc\/chef$/
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "#chef_cache_path" do
59
+ it "sets the path" do
60
+ remote_config.chef_cache_path.should == "/var/chef/cache"
61
+ end
62
+
63
+ it "creates the path remotely" do
64
+ commands_for(:chef_cache_path).tap do |commands|
65
+ commands.should have(1).command
66
+ commands.first.should =~ /mkdir .*? -p \/var\/chef\/cache$/
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#cookbook_paths" do
72
+ it "sets the path" do
73
+ remote_config.cookbook_paths.should have(1).path
74
+ remote_config.cookbook_paths.should =~ ["/var/chef/cookbooks"]
75
+ end
76
+
77
+ it "creates the path remotely" do
78
+ commands_for(:cookbook_paths).tap do |commands|
79
+ commands.should have(1).command
80
+ commands.first.should =~ /mkdir .*? -p \/var\/chef\/cookbooks$/
81
+ end
82
+ end
83
+ end
84
+ end