ridley 0.9.1 → 0.10.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +1 -0
  2. data/.ruby-version +1 -0
  3. data/Guardfile +1 -1
  4. data/README.md +1 -1
  5. data/bootstrappers/{omnibus.erb → unix_omnibus.erb} +20 -5
  6. data/bootstrappers/windows_omnibus.erb +133 -0
  7. data/lib/ridley.rb +4 -1
  8. data/lib/ridley/bootstrap_bindings.rb +5 -0
  9. data/lib/ridley/bootstrap_bindings/unix_template_binding.rb +112 -0
  10. data/lib/ridley/bootstrap_bindings/windows_template_binding.rb +221 -0
  11. data/lib/ridley/bootstrapper.rb +14 -22
  12. data/lib/ridley/bootstrapper/context.rb +53 -162
  13. data/lib/ridley/chef.rb +0 -1
  14. data/lib/ridley/chef/cookbook.rb +0 -9
  15. data/lib/ridley/client.rb +12 -1
  16. data/lib/ridley/errors.rb +1 -0
  17. data/lib/ridley/host_connector.rb +76 -0
  18. data/lib/ridley/{ssh → host_connector}/response.rb +1 -1
  19. data/lib/ridley/{ssh → host_connector}/response_set.rb +11 -11
  20. data/lib/ridley/host_connector/ssh.rb +58 -0
  21. data/lib/ridley/host_connector/ssh/worker.rb +99 -0
  22. data/lib/ridley/host_connector/winrm.rb +55 -0
  23. data/lib/ridley/host_connector/winrm/worker.rb +126 -0
  24. data/lib/ridley/mixin/bootstrap_binding.rb +88 -0
  25. data/lib/ridley/resource.rb +1 -1
  26. data/lib/ridley/resources/client_resource.rb +7 -0
  27. data/lib/ridley/resources/node_resource.rb +9 -4
  28. data/lib/ridley/sandbox_uploader.rb +1 -7
  29. data/lib/ridley/version.rb +1 -1
  30. data/ridley.gemspec +1 -0
  31. data/spec/unit/ridley/bootstrap_bindings/unix_template_binding_spec.rb +102 -0
  32. data/spec/unit/ridley/bootstrap_bindings/windows_template_binding_spec.rb +118 -0
  33. data/spec/unit/ridley/bootstrapper/context_spec.rb +19 -106
  34. data/spec/unit/ridley/bootstrapper_spec.rb +2 -12
  35. data/spec/unit/ridley/client_spec.rb +20 -2
  36. data/spec/unit/ridley/{ssh → host_connector}/response_set_spec.rb +17 -17
  37. data/spec/unit/ridley/host_connector/ssh/worker_spec.rb +15 -0
  38. data/spec/unit/ridley/{ssh_spec.rb → host_connector/ssh_spec.rb} +5 -5
  39. data/spec/unit/ridley/host_connector/winrm/worker_spec.rb +41 -0
  40. data/spec/unit/ridley/host_connector/winrm_spec.rb +47 -0
  41. data/spec/unit/ridley/host_connector_spec.rb +102 -0
  42. data/spec/unit/ridley/mixin/bootstrap_binding_spec.rb +56 -0
  43. data/spec/unit/ridley/sandbox_uploader_spec.rb +1 -28
  44. metadata +53 -25
  45. data/.rbenv-version +0 -1
  46. data/lib/ridley/chef/chefignore.rb +0 -76
  47. data/lib/ridley/ssh.rb +0 -56
  48. data/lib/ridley/ssh/worker.rb +0 -87
  49. data/spec/fixtures/chefignore +0 -8
  50. data/spec/unit/ridley/chef/chefignore_spec.rb +0 -40
  51. data/spec/unit/ridley/ssh/worker_spec.rb +0 -13
@@ -1,5 +1,5 @@
1
1
  module Ridley
2
- class SSH
2
+ module HostConnector
3
3
  # @author Jamie Winsor <reset@riotgames.com>
4
4
  class Response
5
5
  attr_reader :host
@@ -1,15 +1,15 @@
1
1
  module Ridley
2
- class SSH
2
+ module HostConnector
3
3
  # @author Jamie Winsor <reset@riotgames.com>
4
4
  class ResponseSet
5
5
  class << self
6
6
  # Merges the responses of the other ResponseSet with the target ResponseSet
7
7
  # and returns the mutated target
8
8
  #
9
- # @param [SSH::ResponseSet] target
10
- # @param [SSH::ResponseSet] other
9
+ # @param [HostConnector::ResponseSet] target
10
+ # @param [HostConnector::ResponseSet] other
11
11
  #
12
- # @return [SSH::ResponseSet]
12
+ # @return [HostConnector::ResponseSet]
13
13
  def merge!(target, other)
14
14
  if other.is_a?(self)
15
15
  target.add_response(other.responses)
@@ -34,9 +34,9 @@ module Ridley
34
34
  add_response Array(responses)
35
35
  end
36
36
 
37
- # @param [SSH::Response, Array<SSH::Response>] response
37
+ # @param [HostConnector::Response, Array<HostConnector::Response>] response
38
38
  #
39
- # @return [Array<SSH::Response>]
39
+ # @return [Array<HostConnector::Response>]
40
40
  def add_response(response)
41
41
  if response.is_a?(Array)
42
42
  until response.empty?
@@ -64,9 +64,9 @@ module Ridley
64
64
  # Merges the responses of another ResponseSet with self and returns
65
65
  # a new instance of ResponseSet
66
66
  #
67
- # @param [Ridley::SSH::ResponseSet] other
67
+ # @param [Ridley::HostConnector::ResponseSet] other
68
68
  #
69
- # @return [Ridley::SSH::ResponseSet]
69
+ # @return [Ridley::HostConnector::ResponseSet]
70
70
  def merge(other)
71
71
  target = self.class.new(self.responses) # Why the fuck can't I use #dup here?
72
72
  self.class.merge!(target, other)
@@ -75,7 +75,7 @@ module Ridley
75
75
  # Merges the respones of another ResponseSet with self and returns
76
76
  # mutated self
77
77
  #
78
- # @param [Ridley::SSH::ResponseSet] other
78
+ # @param [Ridley::HostConnector::ResponseSet] other
79
79
  #
80
80
  # @return [self]
81
81
  def merge!(other)
@@ -84,12 +84,12 @@ module Ridley
84
84
 
85
85
  private
86
86
 
87
- # @param [SSH::Response] response
87
+ # @param [HostConnector::Response] response
88
88
  def add_failure(response)
89
89
  self.failures << response
90
90
  end
91
91
 
92
- # @param [SSH::Response] response
92
+ # @param [HostConnector::Response] response
93
93
  def add_success(response)
94
94
  self.successes << response
95
95
  end
@@ -0,0 +1,58 @@
1
+ require 'net/ssh'
2
+
3
+ module Ridley
4
+ module HostConnector
5
+ # @author Jamie Winsor <reset@riotgames.com>
6
+ class SSH
7
+ autoload :Worker, 'ridley/host_connector/ssh/worker'
8
+
9
+ class << self
10
+ # @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
11
+ # @param [Hash] options
12
+ def start(nodes, options = {}, &block)
13
+ runner = new(nodes, options)
14
+ result = yield runner
15
+ runner.terminate
16
+
17
+ result
18
+ ensure
19
+ runner.terminate if runner && runner.alive?
20
+ end
21
+ end
22
+
23
+ include Celluloid
24
+ include Celluloid::Logger
25
+
26
+ attr_reader :nodes
27
+ attr_reader :options
28
+
29
+ # @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
30
+ # @param [Hash] options
31
+ # @see Net::SSH
32
+ def initialize(nodes, options = {})
33
+ @nodes = Array(nodes)
34
+ @options = options
35
+ end
36
+
37
+ # @param [String] command
38
+ #
39
+ # @return [Array]
40
+ def run(command)
41
+ workers = Array.new
42
+ futures = self.nodes.collect do |node|
43
+ workers << worker = Worker.new(node.public_hostname, self.options.freeze)
44
+ worker.future.run(command)
45
+ end
46
+
47
+ ResponseSet.new.tap do |response_set|
48
+ futures.each do |future|
49
+ status, response = future.value
50
+ response_set.add_response(response)
51
+ end
52
+ end
53
+ ensure
54
+ workers.map(&:terminate)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,99 @@
1
+ module Ridley
2
+ module HostConnector
3
+ class SSH
4
+ # @author Jamie Winsor <reset@riotgames.com>
5
+ # @api private
6
+ class Worker
7
+ include Celluloid
8
+ include Celluloid::Logger
9
+
10
+ attr_reader :sudo
11
+ attr_reader :user
12
+ attr_reader :host
13
+ # @return [Hashie::Mash]
14
+ attr_reader :options
15
+
16
+ # @param [Hash] options
17
+ def initialize(host, options = {})
18
+ @options = options.deep_symbolize_keys
19
+ @options = options[:ssh] if options[:ssh]
20
+ @host = host
21
+ @sudo = @options[:sudo]
22
+ @user = @options[:user]
23
+
24
+ @options[:paranoid] = false
25
+ end
26
+
27
+ # @param [String] command
28
+ #
29
+ # @return [Array]
30
+ def run(command)
31
+ response = Ridley::HostConnector::Response.new(host)
32
+ debug "Running SSH command: '#{command}' on: '#{host}' as: '#{user}'"
33
+
34
+ channel_exec = ->(channel, command) do
35
+ channel.exec(command) do |ch, success|
36
+ unless success
37
+ raise "Channel execution failed while executing command #{command}"
38
+ end
39
+
40
+ channel.on_data do |ch, data|
41
+ response.stdout += data
42
+ info "NODE[#{host}] #{data}" if data.present? and data != "\r\n"
43
+ end
44
+
45
+ channel.on_extended_data do |ch, type, data|
46
+ response.stderr += data
47
+ info "NODE[#{host}] #{data}" if data.present? and data != "\r\n"
48
+ end
49
+
50
+ channel.on_request("exit-status") do |ch, data|
51
+ response.exit_code = data.read_long
52
+ end
53
+
54
+ channel.on_request("exit-signal") do |ch, data|
55
+ response.exit_signal = data.read_string
56
+ end
57
+ end
58
+ end
59
+
60
+ Net::SSH.start(host, user, options.slice(*Net::SSH::VALID_OPTIONS)) do |ssh|
61
+ ssh.open_channel do |channel|
62
+ if self.sudo
63
+ channel.request_pty do |channel, success|
64
+ raise "Could not aquire pty: A pty is required for running sudo commands." unless success
65
+
66
+ channel_exec.call(channel, command)
67
+ end
68
+ else
69
+ channel_exec.call(channel, command)
70
+ end
71
+ end
72
+
73
+ ssh.loop
74
+ end
75
+
76
+ case response.exit_code
77
+ when 0
78
+ debug "Successfully ran SSH command on: '#{host}' as: '#{user}'"
79
+ [ :ok, response ]
80
+ else
81
+ error "Successfully ran SSH command on: '#{host}' as: '#{user}', but it failed"
82
+ error response.stdout
83
+ [ :error, response ]
84
+ end
85
+ rescue => e
86
+ error "Failed to run SSH command on: '#{host}' as: '#{user}'"
87
+ error "#{e.class}: #{e.message}"
88
+ response.exit_code = -1
89
+ response.stderr = e.message
90
+ [ :error, response ]
91
+ end
92
+
93
+ private
94
+
95
+ attr_reader :runner
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,55 @@
1
+ module Ridley
2
+ module HostConnector
3
+ # @author Kyle Allan <kallan@riotgames.com>
4
+ class WinRM
5
+ autoload :Worker, 'ridley/host_connector/winrm/worker'
6
+
7
+ class << self
8
+ # @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
9
+ # @param [Hash] options
10
+ def start(nodes, options = {}, &block)
11
+ runner = new(nodes, options)
12
+ result = yield runner
13
+ runner.terminate
14
+
15
+ result
16
+ ensure
17
+ runner.terminate if runner && runner.alive?
18
+ end
19
+ end
20
+
21
+ include Celluloid
22
+ include Celluloid::Logger
23
+
24
+ attr_reader :nodes
25
+ attr_reader :options
26
+
27
+ # @param [Ridley::NodeResource, Array<Ridley::NodeResource>] nodes
28
+ # @param [Hash] options
29
+ def initialize(nodes, options = {})
30
+ @nodes = Array(nodes)
31
+ @options = options
32
+ end
33
+
34
+ # @param [String] command
35
+ #
36
+ # @return [Array]
37
+ def run(command)
38
+ workers = Array.new
39
+ futures = self.nodes.collect do |node|
40
+ workers << worker = Worker.new(node.public_hostname, self.options.freeze)
41
+ worker.future.run(command)
42
+ end
43
+
44
+ Ridley::HostConnector::ResponseSet.new.tap do |response_set|
45
+ futures.each do |future|
46
+ status, response = future.value
47
+ response_set.add_response(response)
48
+ end
49
+ end
50
+ ensure
51
+ workers.map(&:terminate)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,126 @@
1
+ module Ridley
2
+ module HostConnector
3
+ class WinRM
4
+ # @author Kyle Allan <kallan@riotgames.com>
5
+ # @api private
6
+ class Worker
7
+ include Celluloid
8
+ include Celluloid::Logger
9
+
10
+ # @return [String]
11
+ attr_reader :user
12
+ # @return [String]
13
+ attr_reader :password
14
+ # @return [String]
15
+ attr_reader :host
16
+ # @return [Hash]
17
+ attr_reader :options
18
+ # @return [String]
19
+ attr_reader :winrm_endpoint
20
+
21
+ # @param host [String]
22
+ # the host the worker is going to work on
23
+ # @option options [Hash] :winrm
24
+ # * :user (String) a user that will login to each node and perform the bootstrap command on (required)
25
+ # * :password (String) the password for the user that will perform the bootstrap
26
+ # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
27
+ def initialize(host, options = {})
28
+ @options = options.deep_symbolize_keys
29
+ @options = options[:winrm] if options[:winrm]
30
+ @host = host
31
+ @user = @options[:user]
32
+ @password = @options[:password]
33
+ @winrm_endpoint = "http://#{host}:#{winrm_port}/wsman"
34
+ end
35
+
36
+ def run(command)
37
+ command = get_command(command)
38
+
39
+ response = Ridley::HostConnector::Response.new(host)
40
+ debug "Running WinRM Command: '#{command}' on: '#{host}' as: '#{user}'"
41
+
42
+ output = winrm.run_cmd(command) do |stdout, stderr|
43
+ response.stdout += stdout unless stdout.nil?
44
+ response.stderr += stderr unless stderr.nil?
45
+ end
46
+ response.exit_code = output[:exitcode]
47
+
48
+ case response.exit_code
49
+ when 0
50
+ debug "Successfully ran WinRM command on: '#{host}' as: '#{user}'"
51
+ [ :ok, response ]
52
+ else
53
+ error "Successfully ran WinRM command on: '#{host}' as: '#{user}', but it failed"
54
+ error response.stdout
55
+ [ :error, response ]
56
+ end
57
+ rescue => e
58
+ error "Failed to run WinRM command on: '#{host}' as: '#{user}'"
59
+ error "#{e.class}: #{e.message}"
60
+ response.exit_code = -1
61
+ response.stderr = e.message
62
+ [ :error, response ]
63
+ end
64
+
65
+ # @return [::WinRM::WinRMWebService]
66
+ def winrm
67
+ ::WinRM::WinRMWebService.new(winrm_endpoint, :plaintext, user: user, pass: password, disable_sspi: true, basic_auth_only: true)
68
+ end
69
+
70
+ # @return [Fixnum]
71
+ def winrm_port
72
+ options[:port] || Ridley::HostConnector::DEFAULT_WINRM_PORT
73
+ end
74
+
75
+ # Returns the command if it does not break the WinRM command length
76
+ # limit. Otherwise, we return an execution of the command as a batch file.
77
+ #
78
+ # @param command [String]
79
+ #
80
+ # @return [String]
81
+ def get_command(command)
82
+ if command.length < 2047
83
+ command
84
+ else
85
+ debug "Detected a command that was longer than 2047 characters, uploading command as a file to the host."
86
+ upload_command_to_host(command)
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # Uploads the command encoded as base64 to a file on the host
93
+ # and then uses Powershell to transform the base64 file into the
94
+ # command that was originally passed through.
95
+ #
96
+ # @param command [String]
97
+ #
98
+ # @return [String] the command to execute the uploaded file
99
+ def upload_command_to_host(command)
100
+ base64_file = "winrm-upload-base64-#{Process.pid}-#{Time.now.to_i}"
101
+ base64_file_name = get_file_path(base64_file)
102
+
103
+ Base64.encode64(command).gsub("\n", '').chars.to_a.each_slice(8000 - base64_file_name.size) do |chunk|
104
+ out = winrm.run_cmd( "echo #{chunk.join} >> \"#{base64_file_name}\"" )
105
+ end
106
+
107
+ command_file = "winrm-upload-#{Process.pid}-#{Time.now.to_i}.bat"
108
+ command_file_name = get_file_path(command_file)
109
+ winrm.powershell <<-POWERSHELL
110
+ $base64_string = Get-Content \"#{base64_file_name}\"
111
+ $bytes = [System.Convert]::FromBase64String($base64_string)
112
+ $new_file = [System.IO.Path]::GetFullPath(\"#{command_file_name}\")
113
+ [System.IO.File]::WriteAllBytes($new_file,$bytes)
114
+ POWERSHELL
115
+
116
+ "cmd.exe /C #{command_file_name}"
117
+ end
118
+
119
+ # @return [String]
120
+ def get_file_path(file)
121
+ (winrm.run_cmd("echo %TEMP%\\#{file}"))[:data][0][:stdout].chomp
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,88 @@
1
+ require 'erubis'
2
+
3
+ module Ridley
4
+ module BootstrapBinding
5
+ module ClassMethods
6
+ def validate_options(options = {})
7
+ if options[:server_url].nil?
8
+ raise Errors::ArgumentError, "A server_url is required for bootstrapping"
9
+ end
10
+
11
+ if options[:validator_path].nil?
12
+ raise Errors::ArgumentError, "A path to a validator is required for bootstrapping"
13
+ end
14
+ end
15
+
16
+ # A hash of default options to be used in the Context initializer
17
+ #
18
+ # @return [Hash]
19
+ def default_options
20
+ @default_options ||= {
21
+ validator_client: "chef-validator",
22
+ attributes: Hash.new,
23
+ run_list: Array.new,
24
+ environment: "_default",
25
+ sudo: true,
26
+ hints: Hash.new
27
+ }
28
+ end
29
+ end
30
+
31
+ class << self
32
+ def included(base)
33
+ base.extend(ClassMethods)
34
+ end
35
+ end
36
+
37
+ attr_reader :template_file
38
+ attr_reader :bootstrap_proxy
39
+ attr_reader :chef_version
40
+ attr_reader :default_options
41
+ attr_reader :validator_path
42
+ attr_reader :encrypted_data_bag_secret_path
43
+ attr_reader :server_url
44
+ attr_reader :validator_client
45
+ attr_reader :node_name
46
+ attr_reader :attributes
47
+ attr_reader :run_list
48
+ attr_reader :environment
49
+
50
+ # @return [Pathname]
51
+ def templates_path
52
+ Ridley.root.join('bootstrappers')
53
+ end
54
+
55
+ # @return [String]
56
+ def first_boot
57
+ MultiJson.encode attributes.merge(run_list: run_list)
58
+ end
59
+
60
+ # @raise [Ridley::Errors::EncryptedDataBagSecretNotFound]
61
+ #
62
+ # @return [String, nil]
63
+ def encrypted_data_bag_secret
64
+ return unless encrypted_data_bag_secret_path
65
+
66
+ IO.read(encrypted_data_bag_secret_path).chomp
67
+ rescue Errno::ENOENT
68
+ raise Errors::EncryptedDataBagSecretNotFound, "Error bootstrapping: Encrypted data bag secret provided but not found at '#{encrypted_data_bag_secret_path}'"
69
+ end
70
+
71
+ # The validation key to create a new client for the node
72
+ #
73
+ # @raise [Ridley::Errors::ValidatorNotFound]
74
+ #
75
+ # @return [String]
76
+ def validation_key
77
+ IO.read(File.expand_path(validator_path)).chomp
78
+ rescue Errno::ENOENT
79
+ raise Errors::ValidatorNotFound, "Error bootstrapping: Validator not found at '#{validator_path}'"
80
+ end
81
+
82
+ # @return [Erubis::Eruby]
83
+ def template
84
+ Erubis::Eruby.new(IO.read(template_file).chomp)
85
+ end
86
+
87
+ end
88
+ end