silver_spurs 0.0.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  require 'silver_spurs/version'
2
2
  require 'silver_spurs/app'
3
+ require 'silver_spurs/asyncifier'
3
4
 
4
5
  module SilverSpurs
5
6
  # I'm a module! Everything important is elsewhere
@@ -1,7 +1,7 @@
1
1
  require 'sinatra/base'
2
2
  require 'silver_spurs/knife_interface'
3
3
  require 'json'
4
-
4
+ require 'silver_spurs/asyncifier'
5
5
 
6
6
  module SilverSpurs
7
7
  class App < Sinatra::Base
@@ -9,8 +9,8 @@ module SilverSpurs
9
9
  set :deployment_key, "/etc/chef/deployment_key.pem"
10
10
  set :deployment_user, "silverspurs"
11
11
  # sane setting for AD subdomain
12
- set :node_name_filter, /^[-A-Za-z0-9]{3,15}$/
13
-
12
+ set :node_name_filter, /^[-A-Za-z0-9]{3,15}$/
13
+
14
14
  get '/' do
15
15
  %q| Ride 'em, cowboy |
16
16
  end
@@ -35,18 +35,56 @@ module SilverSpurs
35
35
  node_name = params[:node_name].strip
36
36
  return 406, {:bad_params => :node_name}.to_json unless node_name =~ settings.node_name_filter
37
37
 
38
- bootstrap_options = Hash[KnifeInterface.supported_arguments.map do |arg|
39
- value = params[arg]
40
- next if value.nil?
41
- [arg, params[arg]]
42
- end]
38
+ process_name = "knife_bootstrap_#{params[:ip].strip.gsub '.', '_'}"
39
+
40
+ unless Asyncifier.has_lock? process_name
41
+ logger.info "Asynchronously spawning knife command. process_name = [#{process_name}]"
42
+ bootstrap_options = Hash[KnifeInterface.supported_arguments.map do |arg|
43
+ value = params[arg]
44
+ next if value.nil?
45
+ [arg, params[arg]]
46
+ end]
43
47
 
44
- result = KnifeInterface.bootstrap(params[:ip], node_name, settings.deployment_user, settings.deployment_key, bootstrap_options)
45
- status_code = result[:exit_code] == 0 ? 201 : 500
48
+ command = KnifeInterface.bootstrap_command(
49
+ params[:ip],
50
+ node_name,
51
+ settings.deployment_user,
52
+ settings.deployment_key,
53
+ bootstrap_options)
54
+ logger.debug "knife command: #{command}"
55
+ Asyncifier.spawn_process process_name, command
56
+ end
46
57
 
47
- return status_code, result.to_json
58
+ redirect to("/bootstrap/query/#{process_name}"), 303
48
59
  end
49
60
 
61
+ head '/bootstrap/query/:process_id' do
62
+ return 404 unless Asyncifier.exists? params[:process_id]
63
+ Asyncifier.reap_old_process params[:process_id]
64
+ return 202 if Asyncifier.has_lock? params[:process_id]
65
+ if Asyncifier.success? params[:process_id]
66
+ return 201
67
+ else
68
+ return 550
69
+ end
70
+ end
71
+
72
+ get '/bootstrap/query/:process_id' do
73
+ return 404 unless Asyncifier.exists? params[:process_id]
74
+ Asyncifier.reap_old_process params[:process_id]
75
+
76
+ headers 'Content-Type' => 'text/plain'
77
+ body Asyncifier.get_log params[:process_id]
78
+ if Asyncifier.success? params[:process_id]
79
+ status 201
80
+ elsif Asyncifier.has_lock? params[:process_id]
81
+ status 202
82
+ else
83
+ status 550
84
+ end
85
+ end
86
+
87
+
50
88
  def required_vars?(params, requirement_list)
51
89
  requirement_list.none? { |required_param| params[required_param].nil? }
52
90
  end
@@ -0,0 +1,100 @@
1
+ require 'singleton'
2
+ require 'forwardable'
3
+
4
+ module SilverSpurs
5
+ class Asyncifier
6
+ include Singleton
7
+ extend SingleForwardable
8
+
9
+ attr_writer :base_path, :timeout
10
+ def_delegators :instance, :has_lock?, :base_path, :base_path=, :timeout=, :timeout, :spawn_process, :has_lock?, :success?, :get_log, :reap_orphaned_lock, :reap_process, :reap_old_process, :exists?
11
+
12
+ def timeout
13
+ @timeout ||= 60 * 60
14
+ end
15
+
16
+ def base_path
17
+ @base_path ||= './silver_spurs_async'
18
+ end
19
+
20
+ def spawn_process(process_name, command)
21
+ create_directory_tree
22
+ logged_command = "#{command} &> #{log_file_path process_name} && touch #{success_file_path process_name}"
23
+ pid = Process.spawn logged_command
24
+ File.open(pid_file_path(process_name), 'wb') { |f| f.write pid }
25
+ Process.detach pid
26
+ end
27
+
28
+ def has_lock?(process_name)
29
+ return false unless File.exists? pid_file_path(process_name)
30
+
31
+ pid = File.read(pid_file_path(process_name)).to_i
32
+ `ps -o command -p #{pid}`.split("\n").count == 2
33
+ end
34
+
35
+ def success?(process_name)
36
+ return false unless File.exists? success_file_path(process_name)
37
+ return true unless File.exists? pid_file_path(process_name)
38
+ File.mtime(success_file_path(process_name)) >= File.mtime(pid_file_path(process_name))
39
+ end
40
+
41
+ def get_log(process_name)
42
+ return nil unless File.exists? log_file_path(process_name)
43
+ File.read log_file_path(process_name)
44
+ end
45
+
46
+ def reap_orphaned_lock(process_name)
47
+ File.delete pid_file_path(process_name) unless has_lock? process_name
48
+ end
49
+
50
+ def reap_process(process_name)
51
+ if has_lock? process_name
52
+ pid = File.read(pid_file_path(process_name)).to_i
53
+ Process.kill 'KILL', pid
54
+ sleep 1
55
+ Process.kill('TERM', pid) if has_lock? process_name
56
+ end
57
+ reap_lock_if_done process_name
58
+ end
59
+
60
+ def reap_old_process(process_name)
61
+ if has_lock? process_name
62
+ launch_time = File.mtime pid_file_path(process_name)
63
+ reap_process process_name if Time.now - launch_time > timeout
64
+ end
65
+ end
66
+
67
+ def exists?(process_name)
68
+ return true if has_lock? process_name
69
+ File.exists? log_file_path(process_name)
70
+ end
71
+
72
+ private
73
+
74
+ def log_file_path(process_name)
75
+ filename = "#{process_name}.log"
76
+ File.join base_path, 'logs', filename
77
+ end
78
+
79
+ def success_file_path(process_name)
80
+ filename = "#{process_name}.success"
81
+ File.join base_path, 'status', filename
82
+ end
83
+
84
+ def pid_file_path(process_name)
85
+ filename = "#{process_name}.pid"
86
+ File.join base_path, 'lockfiles', filename
87
+ end
88
+
89
+ def create_directory_tree
90
+ Dir.mkdir base_path unless Dir.exists? base_path
91
+
92
+ ['logs', 'lockfiles', 'status'].each do |directory|
93
+ path = File.join(base_path, directory)
94
+ Dir.mkdir path unless Dir.exists? path
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+
@@ -3,23 +3,31 @@ require 'addressable/uri'
3
3
  require 'json'
4
4
  require 'silver_spurs/knife_interface'
5
5
  require 'silver_spurs/client/exceptions'
6
+ require 'silver_spurs/client/bootstrap_run'
6
7
 
7
8
  module SilverSpurs
8
9
  class Client
9
10
  def initialize(host_url, options={})
10
11
  @host_url = host_url
11
- @timeout = options[:timeout] || 60 * 60
12
+ @timeout = options[:timeout] || 2 * 60
12
13
  end
13
14
 
14
- def bootstrap(ip, node_name, options = {})
15
+ def start_bootstrap(ip, node_name, options = {})
15
16
  params = extract_extra_params(options).merge({:node_name => node_name})
16
17
  payload = parameterize_hash params
17
18
  headers = {:accept => :json, :content_type=> 'application/x-www-form-urlencoded'}
18
19
 
19
- response = spur_host["bootstrap/#{ip}"].put(payload, headers)
20
- throw ClientException("unexpected response", response) unless response.code == 201
20
+ response = spur_host["bootstrap/#{ip}"].put(payload, headers) do |response, &block|
21
+ if response.code == 303
22
+ response
23
+ else
24
+ response.return! &block
25
+ end
26
+ end
27
+
28
+ throw ClientException.new("unexpected response", response) unless response.code == 303
21
29
 
22
- JSON.parse response.body
30
+ BootstrapRun.new(response.headers[:location], :timeout => @timeout)
23
31
  end
24
32
 
25
33
  private
@@ -0,0 +1,55 @@
1
+ require 'rest-client'
2
+ require 'silver_spurs/client/exceptions'
3
+
4
+ module SilverSpurs
5
+ class BootstrapRun
6
+ def initialize(async_url, options={})
7
+ @async_url = async_url
8
+ @timeout = options[:timeout] || 2 * 60
9
+ end
10
+
11
+ def status
12
+ response = RestClient.head @async_url, &method(:no_exception_for_550)
13
+
14
+ case response.code
15
+ when 201
16
+ :success
17
+ when 202
18
+ :processing
19
+ when 550
20
+ :failed
21
+ when 404
22
+ throw ClientException.new("the server doesn't know anything about this knife run", response)
23
+ else
24
+ throw ClientException.new("unexpected response", response)
25
+ end
26
+ end
27
+
28
+ def log
29
+ response = RestClient.get @async_url, &method(:no_exception_for_550)
30
+
31
+ case response.code
32
+ when 201, 202, 550
33
+ response.body
34
+ when 404
35
+ throw ClientException.new("the server doesn't know anything about this knife run", response)
36
+ else
37
+ throw ClientException.new("unexpected response", response)
38
+ end
39
+ end
40
+
41
+ private
42
+ def no_exception_for_550(response, origin, orig_result, &block)
43
+ if response.code == 550
44
+ response
45
+ else
46
+ response.return! &block
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+
53
+
54
+
55
+
@@ -28,7 +28,7 @@ module SilverSpurs
28
28
  end.reject {|arg| arg.nil?}
29
29
  end
30
30
 
31
- def self.bootstrap(ip, node_name, deployment_user, deployment_key, options = {})
31
+ def self.bootstrap_command(ip, node_name, deployment_user, deployment_key, options = {})
32
32
  bootstrap_options = {
33
33
  :identity_file => deployment_key,
34
34
  :ssh_user => deployment_user,
@@ -36,30 +36,8 @@ module SilverSpurs
36
36
  }.merge options
37
37
 
38
38
  arguments = expand_bootstrap_args bootstrap_options
39
- logger.debug "Knife arguments: #{arguments.join ', '}"
40
-
41
- strap_r, strap_w = IO.pipe
42
39
 
43
- command = ['knife', 'bootstrap', *arguments, ip].join ' '
44
- logger.debug "Knife command line: #{command}"
45
- knife_pid = spawn(command, :err => :out, :out => strap_w)
46
-
47
- Process.waitpid(knife_pid)
48
- exitcode = $?.exitstatus
49
-
50
- strap_w.close
51
- loglines = strap_r.read
52
- logger.debug "Knife log lines: #{loglines}"
53
- strap_r.close
54
-
55
- {
56
- :exit_code => exitcode,
57
- :log_lines => loglines
58
- }
59
- end
60
-
61
- def self.logger
62
- @logger ||= Logger.new(STDERR)
40
+ command = ['knife', 'bootstrap', *arguments, ip].join ' '
63
41
  end
64
42
 
65
43
  end
@@ -1,3 +1,3 @@
1
1
  module SilverSpurs
2
- VERSION = "0.0.8"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: silver_spurs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-15 00:00:00.000000000 Z
12
+ date: 2013-03-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
@@ -187,7 +187,9 @@ files:
187
187
  - Rakefile
188
188
  - lib/silver_spurs.rb
189
189
  - lib/silver_spurs/app.rb
190
+ - lib/silver_spurs/asyncifier.rb
190
191
  - lib/silver_spurs/client.rb
192
+ - lib/silver_spurs/client/bootstrap_run.rb
191
193
  - lib/silver_spurs/client/exceptions.rb
192
194
  - lib/silver_spurs/knife_interface.rb
193
195
  - lib/silver_spurs/version.rb