silver_spurs 1.0.1 → 1.0.2
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.
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.simplecov +4 -0
- data/.travis.yml +6 -0
- data/README.md +55 -8
- data/Rakefile +18 -0
- data/lib/silver_spurs/asyncifier.rb +3 -3
- data/lib/silver_spurs/client.rb +10 -8
- data/lib/silver_spurs/client/bootstrap_run.rb +1 -4
- data/lib/silver_spurs/knife_interface.rb +1 -1
- data/lib/silver_spurs/version.rb +1 -1
- data/silver_spurs.gemspec +1 -0
- data/spec/lib/silver_spurs/app_spec.rb +192 -9
- data/spec/lib/silver_spurs/asyncifier_spec.rb +509 -0
- data/spec/lib/silver_spurs/client/bootstrap_run_spec.rb +162 -0
- data/spec/lib/silver_spurs/client_spec.rb +113 -0
- data/spec/lib/silver_spurs/knife_interface_spec.rb +26 -1
- data/spec/spec_helper.rb +2 -0
- metadata +33 -3
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.simplecov
ADDED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,29 +1,76 @@
|
|
1
|
-
# SilverSpurs
|
1
|
+
# SilverSpurs [](https://travis-ci.org/christian-blades-cb/silver_spurs)
|
2
2
|
|
3
|
-
|
3
|
+
RESTful Chef bootstrapping
|
4
|
+
|
5
|
+
Instead of using a CLI to kick off bootstrapping, hit an API endpoint and let the service do it for you.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
9
|
+
Install chef and set up knife on the machine (refer to the [chef docs](http://docs.opscode.com))
|
10
|
+
|
7
11
|
Add this line to your application's Gemfile:
|
8
12
|
|
9
13
|
gem 'silver_spurs'
|
10
14
|
|
15
|
+
Create a config.ru:
|
16
|
+
|
17
|
+
require 'bundler'
|
18
|
+
Bundler.setup
|
19
|
+
|
20
|
+
require 'silver_spurs'
|
21
|
+
|
22
|
+
# SilverSpurs::App.deployment_key = '/opt/deployment_key.pem'
|
23
|
+
# SilverSpurs::App.deployment_user = 'deployer'
|
24
|
+
# SilverSpurs::App.node_name_filter = /\w{3,10}/
|
25
|
+
# SilverSpurs::Asyncifier.base_path = '/opt/silver_spurs'
|
26
|
+
|
27
|
+
run SilverSpurs::App
|
28
|
+
|
11
29
|
And then execute:
|
12
30
|
|
13
31
|
$ bundle
|
32
|
+
$ rackup
|
14
33
|
|
15
|
-
|
34
|
+
## Usage
|
16
35
|
|
17
|
-
|
36
|
+
### Kick off a bootstrap
|
18
37
|
|
19
|
-
|
38
|
+
$ curl -X PUT -i -d node_name=machineotron http://localhost/bootstrap/10.0.1.2
|
39
|
+
|
40
|
+
The redirect will point to the query URL for the bootstrap run. Since these runs tend to take several minutes, this is preferable to a long-running HTTP request.
|
41
|
+
|
42
|
+
Further GET requests to this endpoint will return a log of the run in progress. HEAD requests will return just the status code.
|
43
|
+
|
44
|
+
### Status codes
|
45
|
+
|
46
|
+
* 406 - Missing a required parameter
|
47
|
+
* 404 - Unknown endpoint or resource
|
48
|
+
* 201 - Run completed successfully
|
49
|
+
* 202 - Run in progress
|
50
|
+
* 550 - Run ended in failure
|
51
|
+
|
52
|
+
## Using the client
|
53
|
+
|
54
|
+
Add this line to the application's Gemfile:
|
55
|
+
|
56
|
+
gem 'silver_spurs'
|
57
|
+
|
58
|
+
Sample application
|
20
59
|
|
21
|
-
|
60
|
+
require 'silver_spurs/client'
|
61
|
+
|
62
|
+
silver = SilverSpurs::Client.new('http://localhost')
|
63
|
+
bootstrap = silver.start_bootstrap('10.0.1.2', 'machineotron')
|
64
|
+
while bootstrap.status == :pending do
|
65
|
+
sleep 10
|
66
|
+
end
|
67
|
+
puts bootstrap.log
|
22
68
|
|
23
69
|
## Contributing
|
24
70
|
|
25
71
|
1. Fork it
|
26
72
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
73
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
-
4.
|
29
|
-
5.
|
74
|
+
4. Add you some tests
|
75
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
76
|
+
6. Create new Pull Request
|
data/Rakefile
CHANGED
@@ -1,2 +1,20 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
2
|
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
desc 'Run all tests'
|
6
|
+
task :test => [:spec]
|
7
|
+
task :default => [:coverage]
|
8
|
+
|
9
|
+
desc "Run all rspec tests"
|
10
|
+
RSpec::Core::RakeTask.new 'spec' do |t|
|
11
|
+
t.pattern = 'spec/**/*_spec.rb'
|
12
|
+
t.rspec_opts = '-fd -c'
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Run all rspec tests and generate a coverage report"
|
16
|
+
task :coverage do
|
17
|
+
ENV['COVERAGE'] = 'true'
|
18
|
+
Rake::Task['spec'].invoke
|
19
|
+
`open coverage/index.html` if RUBY_PLATFORM.downcase.include? 'darwin'
|
20
|
+
end
|
@@ -28,7 +28,7 @@ module SilverSpurs
|
|
28
28
|
def spawn_process(process_name, command)
|
29
29
|
create_directory_tree
|
30
30
|
logged_command = "#{command} 1> #{log_file_path process_name} 2>&1 && touch #{success_file_path process_name}"
|
31
|
-
logger.debug "Executing: #{
|
31
|
+
logger.debug "Executing: #{logged_command}"
|
32
32
|
pid = Process.spawn logged_command
|
33
33
|
File.open(pid_file_path(process_name), 'wb') { |f| f.write pid }
|
34
34
|
Process.detach pid
|
@@ -38,7 +38,7 @@ module SilverSpurs
|
|
38
38
|
return false unless File.exists? pid_file_path(process_name)
|
39
39
|
|
40
40
|
pid = File.read(pid_file_path(process_name)).to_i
|
41
|
-
|
41
|
+
IO.popen("ps -o command -p #{pid}").read.split("\n").count == 2
|
42
42
|
end
|
43
43
|
|
44
44
|
def success?(process_name)
|
@@ -63,7 +63,7 @@ module SilverSpurs
|
|
63
63
|
sleep 1
|
64
64
|
Process.kill('TERM', pid) if has_lock? process_name
|
65
65
|
end
|
66
|
-
|
66
|
+
reap_orphaned_lock process_name
|
67
67
|
end
|
68
68
|
|
69
69
|
def reap_old_process(process_name)
|
data/lib/silver_spurs/client.rb
CHANGED
@@ -17,13 +17,7 @@ module SilverSpurs
|
|
17
17
|
payload = parameterize_hash params
|
18
18
|
headers = {:accept => :json, :content_type=> 'application/x-www-form-urlencoded'}
|
19
19
|
|
20
|
-
response = spur_host["bootstrap/#{ip}"].put(payload, headers
|
21
|
-
if response.code == 303
|
22
|
-
response
|
23
|
-
else
|
24
|
-
response.return! &block
|
25
|
-
end
|
26
|
-
end
|
20
|
+
response = spur_host["bootstrap/#{ip}"].put(payload, headers, &method(:dont_redirect_for_303))
|
27
21
|
|
28
22
|
throw ClientException.new("unexpected response", response) unless response.code == 303
|
29
23
|
|
@@ -46,6 +40,14 @@ module SilverSpurs
|
|
46
40
|
def spur_host
|
47
41
|
RestClient::Resource.new(@host_url, :timeout => @timeout)
|
48
42
|
end
|
49
|
-
|
43
|
+
|
44
|
+
def dont_redirect_for_303(response, origin, orig_result, &block)
|
45
|
+
if response.code == 303
|
46
|
+
response
|
47
|
+
else
|
48
|
+
response.return! &block
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
50
52
|
end
|
51
53
|
end
|
data/lib/silver_spurs/version.rb
CHANGED
data/silver_spurs.gemspec
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'json'
|
2
3
|
|
3
4
|
describe SilverSpurs::App do
|
4
5
|
include Rack::Test::Methods
|
@@ -6,11 +7,54 @@ describe SilverSpurs::App do
|
|
6
7
|
def app
|
7
8
|
SilverSpurs::App
|
8
9
|
end
|
10
|
+
|
11
|
+
describe '/' do
|
12
|
+
it "should return 200" do
|
13
|
+
get '/'
|
14
|
+
last_response.status.should be 200
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '/settings' do
|
19
|
+
it "should tell us about the deployment_key, deployment_user and node_name_filter" do
|
20
|
+
get '/settings'
|
21
|
+
settings_hash = JSON.parse last_response.body
|
22
|
+
[:deployment_key, :deployment_user, :node_name_filter].each do |setting|
|
23
|
+
settings_hash.keys.should include setting.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
9
27
|
|
10
28
|
describe "/bootstrap/:ip" do
|
29
|
+
|
30
|
+
context "when the node is not already being bootstrapped" do
|
31
|
+
before :each do
|
32
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "builds a bootstrap command" do
|
36
|
+
SilverSpurs::Asyncifier.stub(:spawn_process)
|
37
|
+
SilverSpurs::KnifeInterface.should_receive(:bootstrap_command).with('10.0.0.0', 'yourmom', kind_of(String), kind_of(String), kind_of(Hash))
|
38
|
+
|
39
|
+
put '/bootstrap/10.0.0.0', :node_name => 'yourmom'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "spawns a knife run" do
|
43
|
+
SilverSpurs::Asyncifier.should_receive(:spawn_process).with(kind_of(String), kind_of(String))
|
44
|
+
SilverSpurs::KnifeInterface.stub(:bootstrap_command).and_return 'knife bootstrap'
|
45
|
+
|
46
|
+
put '/bootstrap/10.0.0.0', :node_name => 'yourmom'
|
47
|
+
end
|
48
|
+
end
|
11
49
|
|
50
|
+
context "when a node name is not passed in" do
|
51
|
+
it "should return a 406 status" do
|
52
|
+
put '/bootstrap/10.0.0.0'
|
53
|
+
last_response.status.should be 406
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
12
57
|
context "with bad node name" do
|
13
|
-
|
14
58
|
it "should reject node names with spaces" do
|
15
59
|
node_name = "your mom"
|
16
60
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
@@ -46,47 +90,186 @@ describe SilverSpurs::App do
|
|
46
90
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
47
91
|
last_response.status.should eq 406
|
48
92
|
end
|
49
|
-
|
50
93
|
end
|
51
94
|
|
52
95
|
context "with good node name" do
|
53
96
|
before(:each) do
|
54
|
-
SilverSpurs::
|
97
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return true
|
55
98
|
end
|
56
99
|
|
57
100
|
it "should accept node names with dashes" do
|
58
101
|
node_name = "your-mom"
|
59
102
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
60
|
-
last_response.status.should eq
|
103
|
+
last_response.status.should eq 303
|
61
104
|
end
|
62
105
|
|
63
106
|
it "should accept one-word node names" do
|
64
107
|
node_name = "mom"
|
65
108
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
66
|
-
last_response.status.should eq
|
109
|
+
last_response.status.should eq 303
|
67
110
|
end
|
68
111
|
|
69
112
|
it "should accept node names with numbers" do
|
70
113
|
node_name = "mombot-3000"
|
71
114
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
72
|
-
last_response.status.should eq
|
115
|
+
last_response.status.should eq 303
|
73
116
|
end
|
74
117
|
|
75
118
|
it "should accept node names with 15 chars" do
|
76
119
|
node_name = "123456789012345"
|
77
120
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
78
|
-
last_response.status.should eq
|
121
|
+
last_response.status.should eq 303
|
79
122
|
end
|
80
123
|
|
81
124
|
it "should accept node names with 3 chars" do
|
82
125
|
node_name = "123"
|
83
126
|
put "/bootstrap/10.0.0.0", :node_name => node_name
|
84
|
-
last_response.status.should eq
|
127
|
+
last_response.status.should eq 303
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '/bootstrap/query/:process_id' do
|
134
|
+
|
135
|
+
describe 'HEAD' do
|
136
|
+
|
137
|
+
context 'when process does not exist' do
|
138
|
+
before :each do
|
139
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return false
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should return a 404' do
|
143
|
+
head '/bootstrap/query/fake_process'
|
144
|
+
last_response.status.should eq 404
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'when process is still running' do
|
149
|
+
before :each do
|
150
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return true
|
151
|
+
SilverSpurs::Asyncifier.stub(:reap_old_process)
|
152
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return true
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should return a 202' do
|
156
|
+
head '/bootstrap/query/fake_process'
|
157
|
+
last_response.status.should eq 202
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'when process finished successfully' do
|
162
|
+
before :each do
|
163
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return true
|
164
|
+
SilverSpurs::Asyncifier.stub(:reap_old_process)
|
165
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return false
|
166
|
+
SilverSpurs::Asyncifier.stub(:success?).and_return true
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should return a 201' do
|
170
|
+
head '/bootstrap/query/fake_process'
|
171
|
+
last_response.status.should eq 201
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'when process finished in failure' do
|
176
|
+
before :each do
|
177
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return true
|
178
|
+
SilverSpurs::Asyncifier.stub(:reap_old_process)
|
179
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return false
|
180
|
+
SilverSpurs::Asyncifier.stub(:success?).and_return false
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'should return a 550' do
|
184
|
+
head '/bootstrap/query/fake_process'
|
185
|
+
last_response.status.should eq 550
|
186
|
+
end
|
85
187
|
end
|
86
|
-
|
188
|
+
|
87
189
|
end
|
190
|
+
|
191
|
+
describe 'GET' do
|
192
|
+
before :each do
|
193
|
+
SilverSpurs::Asyncifier.stub(:get_log).and_return "Loggylog"
|
194
|
+
end
|
195
|
+
|
196
|
+
context 'when process does not exist' do
|
197
|
+
before :each do
|
198
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return false
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should return a 404' do
|
202
|
+
get '/bootstrap/query/fake_process'
|
203
|
+
last_response.status.should eq 404
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should not spit out a log' do
|
207
|
+
get '/bootstrap/query/fake_process'
|
208
|
+
last_response.body.should_not eq 'Loggylog'
|
209
|
+
end
|
88
210
|
|
211
|
+
end
|
212
|
+
|
213
|
+
context 'when process is still running' do
|
214
|
+
before :each do
|
215
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return true
|
216
|
+
SilverSpurs::Asyncifier.stub(:reap_old_process)
|
217
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return true
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'should return a 202' do
|
221
|
+
get '/bootstrap/query/fake_process'
|
222
|
+
last_response.status.should eq 202
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'should spit out a log' do
|
226
|
+
get '/bootstrap/query/fake_process'
|
227
|
+
last_response.body.should eq 'Loggylog'
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context 'when process finished successfully' do
|
232
|
+
before :each do
|
233
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return true
|
234
|
+
SilverSpurs::Asyncifier.stub(:reap_old_process)
|
235
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return false
|
236
|
+
SilverSpurs::Asyncifier.stub(:success?).and_return true
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'should return a 201' do
|
240
|
+
get '/bootstrap/query/fake_process'
|
241
|
+
last_response.status.should eq 201
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'should spit out a log' do
|
245
|
+
get '/bootstrap/query/fake_process'
|
246
|
+
last_response.body.should eq 'Loggylog'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context 'when process finished in failure' do
|
251
|
+
before :each do
|
252
|
+
SilverSpurs::Asyncifier.stub(:exists?).and_return true
|
253
|
+
SilverSpurs::Asyncifier.stub(:reap_old_process)
|
254
|
+
SilverSpurs::Asyncifier.stub(:has_lock?).and_return false
|
255
|
+
SilverSpurs::Asyncifier.stub(:success?).and_return false
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'should return a 550' do
|
259
|
+
get '/bootstrap/query/fake_process'
|
260
|
+
last_response.status.should eq 550
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'should spit out a log' do
|
264
|
+
get '/bootstrap/query/fake_process'
|
265
|
+
last_response.body.should eq 'Loggylog'
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
89
271
|
end
|
272
|
+
|
90
273
|
end
|
91
274
|
|
92
275
|
|