ridley 0.9.1 → 0.10.0.rc1

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