silver_spurs 0.0.8 → 1.0.0

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.
@@ -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