ridley 0.10.0.rc1 → 0.10.0.rc2

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.
@@ -25,10 +25,6 @@ setlocal
25
25
  <%= "SETX HTTP_PROXY \"#{bootstrap_proxy}\"" if bootstrap_proxy %>
26
26
  mkdir <%= bootstrap_directory %>
27
27
 
28
- > <%= bootstrap_directory %>\wget.vbs (
29
- <%= windows_wget_vb %>
30
- )
31
-
32
28
  > <%= bootstrap_directory %>\wget.ps1 (
33
29
  <%= windows_wget_powershell %>
34
30
  )
@@ -87,24 +83,7 @@ set "REMOTE_SOURCE_MSI_URL=https://www.opscode.com/chef/download?p=windows&pv=%M
87
83
  set "LOCAL_DESTINATION_MSI_PATH=<%= local_download_path %>"
88
84
  set "FALLBACK_QUERY_STRING=&DownloadContext=PowerShell"
89
85
 
90
- cscript /nologo <%= bootstrap_directory %>\wget.vbs /url:"%REMOTE_SOURCE_MSI_URL%" /path:"%LOCAL_DESTINATION_MSI_PATH%"
91
-
92
- rem Work around issues found in Windows Server 2012 around job objects not respecting WSMAN memory quotas
93
- rem that cause the MSI download process to exceed the quota even when it is increased by administrators.
94
- rem Retry the download using a more memory-efficient mechanism that only works if PowerShell is available.
95
- if ERRORLEVEL 1 (
96
- echo Warning: Failed to download "%REMOTE_SOURCE_MSI_URL%" to "%LOCAL_DESTINATION_MSI_PATH%"
97
- echo Warning: Retrying download with PowerShell if available
98
- if EXIST "%LOCAL_DESTINATION_MSI_PATH%" del /f /q "%LOCAL_DESTINATION_MSI_PATH%"
99
- powershell -ExecutionPolicy Unrestricted -NoProfile -NonInteractive "& '<%= bootstrap_directory %>\wget.ps1' '%REMOTE_SOURCE_MSI_URL%%FALLBACK_QUERY_STRING%' '%LOCAL_DESTINATION_MSI_PATH%'"
100
-
101
- if NOT ERRORLEVEL 1 (
102
- echo Download succeeded
103
- ) else (
104
- echo Failed to download "%REMOTE_SOURCE_MSI_URL%"
105
- echo Subsequent attempt to install the downloaded MSI is likely to fail
106
- )
107
- )
86
+ powershell -ExecutionPolicy Unrestricted -NoProfile -NonInteractive "& '<%= bootstrap_directory %>\wget.ps1' '%REMOTE_SOURCE_MSI_URL%%FALLBACK_QUERY_STRING%' '%LOCAL_DESTINATION_MSI_PATH%'"
108
87
 
109
88
  <%= install_chef %>
110
89
 
@@ -119,59 +119,6 @@ CONFIG
119
119
  raise Errors::EncryptedDataBagSecretNotFound, "Error bootstrapping: Encrypted data bag secret provided but not found at '#{encrypted_data_bag_secret_path}'"
120
120
  end
121
121
 
122
- # Implements a Visual Basic script that attempts a simple
123
- # 'wget' to download the Chef msi
124
- #
125
- # @return [String]
126
- def windows_wget_vb
127
- win_wget = <<-WGET
128
- url = WScript.Arguments.Named("url")
129
- path = WScript.Arguments.Named("path")
130
- proxy = null
131
- Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP")
132
- Set wshShell = CreateObject( "WScript.Shell" )
133
- Set objUserVariables = wshShell.Environment("USER")
134
-
135
- 'http proxy is optional
136
- 'attempt to read from HTTP_PROXY env var first
137
- On Error Resume Next
138
-
139
- If NOT (objUserVariables("HTTP_PROXY") = "") Then
140
- proxy = objUserVariables("HTTP_PROXY")
141
-
142
- 'fall back to named arg
143
- ElseIf NOT (WScript.Arguments.Named("proxy") = "") Then
144
- proxy = WScript.Arguments.Named("proxy")
145
- End If
146
-
147
- If NOT isNull(proxy) Then
148
- 'setProxy method is only available on ServerXMLHTTP 6.0+
149
- Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
150
- objXMLHTTP.setProxy 2, proxy
151
- End If
152
-
153
- On Error Goto 0
154
-
155
- objXMLHTTP.open "GET", url, false
156
- objXMLHTTP.send()
157
- If objXMLHTTP.Status = 200 Then
158
- Set objADOStream = CreateObject("ADODB.Stream")
159
- objADOStream.Open
160
- objADOStream.Type = 1
161
- objADOStream.Write objXMLHTTP.ResponseBody
162
- objADOStream.Position = 0
163
- Set objFSO = Createobject("Scripting.FileSystemObject")
164
- If objFSO.Fileexists(path) Then objFSO.DeleteFile path
165
- Set objFSO = Nothing
166
- objADOStream.SaveToFile path
167
- objADOStream.Close
168
- Set objADOStream = Nothing
169
- End if
170
- Set objXMLHTTP = Nothing
171
- WGET
172
- escape_and_echo(win_wget)
173
- end
174
-
175
122
  # Implements a Powershell script that attempts a simple
176
123
  # 'wget' to download the Chef msi
177
124
  #
@@ -4,14 +4,11 @@ module Ridley
4
4
  autoload :Context, 'ridley/bootstrapper/context'
5
5
 
6
6
  include Celluloid
7
- include Celluloid::Logger
8
-
7
+ include Ridley::Logging
8
+
9
9
  # @return [Array<String>]
10
10
  attr_reader :hosts
11
11
 
12
- # @return [Array<Bootstrapper::Context>]
13
- attr_reader :contexts
14
-
15
12
  # @return [Hash]
16
13
  attr_reader :options
17
14
 
@@ -47,7 +44,7 @@ module Ridley
47
44
  # @option options [String] :template
48
45
  # bootstrap template to use
49
46
  def initialize(hosts, options = {})
50
- @hosts = Array(hosts).collect(&:to_s).uniq
47
+ @hosts = Array(hosts).flatten.collect(&:to_s).uniq
51
48
  @options = options.dup
52
49
  @options[:ssh] ||= Hash.new
53
50
  @options[:ssh] = {
@@ -56,16 +53,22 @@ module Ridley
56
53
  }.merge(@options[:ssh])
57
54
 
58
55
  @options[:sudo] = @options[:ssh][:sudo]
59
- @contexts = @hosts.collect do |host|
60
- Context.create(host, options)
61
- end
62
56
  end
63
57
 
58
+ # @raise [Errors::HostConnectionError] if a node is unreachable
59
+ #
60
+ # @return [Array<Bootstrapper::Context>]
61
+ def contexts
62
+ @contexts ||= @hosts.collect { |host| Context.create(host, options) }
63
+ end
64
+
65
+ # @raise [Errors::HostConnectionError] if a node is unreachable
66
+ #
64
67
  # @return [HostConnector::ResponseSet]
65
68
  def run
66
69
  workers = Array.new
67
70
  futures = contexts.collect do |context|
68
- info "Running bootstrap command on #{context.host}"
71
+ log.info { "Running bootstrap command on #{context.host}" }
69
72
 
70
73
  workers << worker = context.host_connector::Worker.new(context.host, self.options.freeze)
71
74
 
@@ -5,7 +5,6 @@ module Ridley
5
5
  # @author Jamie Winsor <reset@riotgames.com>
6
6
  class Context
7
7
  class << self
8
-
9
8
  # @param [String] host
10
9
  # @option options [Hash] :ssh
11
10
  # * :user (String) a shell user that will login to each node and perform the bootstrap command on (required)
@@ -38,6 +37,8 @@ module Ridley
38
37
  # bootstrap with sudo (default: true)
39
38
  # @option options [String] :template ('omnibus')
40
39
  # bootstrap template to use
40
+ #
41
+ # @raise [Errors::HostConnectionError] if a node is unreachable
41
42
  def create(host, options = {})
42
43
  host_connector = HostConnector.best_connector_for(host, options)
43
44
  template_binding = case host_connector.to_s
data/lib/ridley/errors.rb CHANGED
@@ -40,6 +40,9 @@ module Ridley
40
40
  class EncryptedDataBagSecretNotFound < BootstrapError; end
41
41
  class HostConnectionError < BootstrapError; end
42
42
 
43
+ class RemoteCommandError < RidleyError; end
44
+ class RemoteScriptError < RemoteCommandError; end
45
+
43
46
  # Exception thrown when the maximum amount of requests is exceeded.
44
47
  class RedirectLimitReached < RidleyError
45
48
  attr_reader :response
@@ -14,24 +14,32 @@ module Ridley
14
14
 
15
15
  class << self
16
16
  # Finds and returns the best HostConnector for a given host
17
- #
17
+ #
18
18
  # @param host [String]
19
19
  # the host to attempt to connect to
20
20
  # @option options [Hash] :ssh
21
21
  # * :port (Fixnum) the ssh port to connect on the node the bootstrap will be performed on (22)
22
22
  # @option options [Hash] :winrm
23
23
  # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
24
- #
24
+ # @param block [Proc]
25
+ # an optional block that is yielded the best HostConnector
26
+ #
25
27
  # @return [Ridley::HostConnector] a class under Ridley::HostConnector
26
- def best_connector_for(host, options = {})
28
+ def best_connector_for(host, options = {}, &block)
27
29
  ssh_port, winrm_port = parse_port_options(options)
28
30
  if connector_port_open?(host, ssh_port)
29
- Ridley::HostConnector::SSH
31
+ host_connector = Ridley::HostConnector::SSH
30
32
  elsif connector_port_open?(host, winrm_port)
31
- Ridley::HostConnector::WinRM
33
+ host_connector = Ridley::HostConnector::WinRM
32
34
  else
33
35
  raise Ridley::Errors::HostConnectionError, "No available connection method available on #{host}."
34
36
  end
37
+
38
+ if block_given?
39
+ yield host_connector
40
+ else
41
+ host_connector
42
+ end
35
43
  end
36
44
 
37
45
  # Checks to see if the given port is open for TCP connections
@@ -41,19 +49,16 @@ module Ridley
41
49
  # the host to attempt to connect to
42
50
  # @param port [Fixnum]
43
51
  # the port to attempt to connect on
44
- #
52
+ #
45
53
  # @return [Boolean]
46
54
  def connector_port_open?(host, port)
47
- Timeout::timeout(1) do
48
- begin
49
- socket = TCPSocket.new(host, port)
50
- socket.close
51
- true
52
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
53
- false
54
- end
55
+ Timeout::timeout(3) do
56
+ socket = TCPSocket.new(host, port)
57
+ socket.close
55
58
  end
56
- rescue Timeout::Error
59
+
60
+ true
61
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
57
62
  false
58
63
  end
59
64
 
@@ -64,7 +69,7 @@ module Ridley
64
69
  # * :port (Fixnum) the ssh port to connect on the node the bootstrap will be performed on (22)
65
70
  # @option options [Hash] :winrm
66
71
  # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
67
- #
72
+ #
68
73
  # @return [Array]
69
74
  def parse_port_options(options)
70
75
  ssh_port = options[:ssh][:port] if options[:ssh]
@@ -12,11 +12,13 @@ module Ridley
12
12
  attr_reader :host
13
13
  # @return [Hashie::Mash]
14
14
  attr_reader :options
15
+
16
+ EMBEDDED_RUBY_PATH = '/opt/chef/embedded/bin/ruby'.freeze
15
17
 
16
18
  # @param [Hash] options
17
19
  def initialize(host, options = {})
18
- @options = options.deep_symbolize_keys
19
- @options = options[:ssh] if options[:ssh]
20
+ options = options.deep_symbolize_keys
21
+ @options = options[:ssh] || Hash.new
20
22
  @host = host
21
23
  @sudo = @options[:sudo]
22
24
  @user = @options[:user]
@@ -90,6 +92,42 @@ module Ridley
90
92
  [ :error, response ]
91
93
  end
92
94
 
95
+ # Executes a chef-client command on the nodes
96
+ #
97
+ # @return [#run]
98
+ def chef_client
99
+ command = "chef-client"
100
+ if sudo
101
+ command = "sudo #{command}"
102
+ end
103
+
104
+ run(command)
105
+ end
106
+
107
+ # Executes a copy of the encrypted_data_bag_secret to the nodes
108
+ #
109
+ # @param [String] encrypted_data_bag_secret_path
110
+ # the path to the encrypted_data_bag_secret
111
+ #
112
+ # @return [#run]
113
+ def put_secret(encrypted_data_bag_secret_path)
114
+ secret = File.read(encrypted_data_bag_secret_path).chomp
115
+ command = "echo '#{secret}' > /etc/chef/encrypted_data_bag_secret; chmod 0600 /etc/chef/encrypted_data_bag_secret"
116
+
117
+ run(command)
118
+ end
119
+
120
+ # Executes a provided Ruby script in the embedded Ruby installation
121
+ #
122
+ # @param [Array<String>] command_lines
123
+ # An Array of lines of the command to be executed
124
+ #
125
+ # @return [#run]
126
+ def ruby_script(command_lines)
127
+ command = "#{EMBEDDED_RUBY_PATH} -e \"#{command_lines.join(';')}\""
128
+ run(command)
129
+ end
130
+
93
131
  private
94
132
 
95
133
  attr_reader :runner
@@ -2,6 +2,7 @@ module Ridley
2
2
  module HostConnector
3
3
  # @author Kyle Allan <kallan@riotgames.com>
4
4
  class WinRM
5
+ autoload :CommandUploader, 'ridley/host_connector/winrm/command_uploader'
5
6
  autoload :Worker, 'ridley/host_connector/winrm/worker'
6
7
 
7
8
  class << self
@@ -0,0 +1,90 @@
1
+ module Ridley
2
+ module HostConnector
3
+ class WinRM
4
+ # @author Kyle Allan <kallan@riotgames.com>
5
+ # @author Justin Campbell <justin.campbell@riotgames.com>
6
+ #
7
+ # @example
8
+ # command_uploader = CommandUploader.new(long_command, winrm)
9
+ # command_uploader.upload
10
+ # command_uploader.command
11
+ #
12
+ # This class is used by WinRM Workers when the worker is told to execute a command that
13
+ # might be too long for WinRM to handle.
14
+ #
15
+ # After an instance of this class is created, the upload method will upload the long command
16
+ # to the node being worked on in chunks. Once on the node, some Powershell code will decode
17
+ # the long_command into a batch file. The command method will return a String representing the
18
+ # command the worker will use to execute the uploaded batch script.
19
+ class CommandUploader
20
+ CHUNK_LIMIT = 1024
21
+
22
+ # @return [WinRM::WinRMWebService]
23
+ attr_reader :winrm
24
+ # @return [String]
25
+ attr_reader :base64_file_name
26
+ # @return [String]
27
+ attr_reader :command_file_name
28
+
29
+ # @param [WinRM::WinRMWebService] winrm
30
+ def initialize(winrm)
31
+ @winrm = winrm
32
+ @base64_file_name = get_file_path("winrm-upload-base64-#{unique_string}")
33
+ @command_file_name = get_file_path("winrm-upload-#{unique_string}.bat")
34
+ end
35
+
36
+ # Uploads the command encoded as base64 to a file on the host
37
+ # and then uses Powershell to transform the base64 file into the
38
+ # command that was originally passed through.
39
+ #
40
+ # @param [String] command_string
41
+ def upload(command_string)
42
+ upload_command(command_string)
43
+ convert_command
44
+ end
45
+
46
+ # @return [String] the command to execute the uploaded file
47
+ def command
48
+ "cmd.exe /C #{command_file_name}"
49
+ end
50
+
51
+ # Runs a delete command on the files generated by #base64_file_name
52
+ # and #command_file_name
53
+ def cleanup
54
+ winrm.run_cmd( "del #{base64_file_name} /F /Q" )
55
+ winrm.run_cmd( "del #{command_file_name} /F /Q" )
56
+ end
57
+
58
+ private
59
+
60
+ def upload_command(command_string)
61
+ command_string_chars(command_string).each_slice(CHUNK_LIMIT) do |chunk|
62
+ winrm.run_cmd( "echo #{chunk.join} >> \"#{base64_file_name}\"" )
63
+ end
64
+ end
65
+
66
+ def command_string_chars(command_string)
67
+ Base64.encode64(command_string).gsub("\n", '').chars.to_a
68
+ end
69
+
70
+ def convert_command
71
+ winrm.powershell <<-POWERSHELL
72
+ $base64_string = Get-Content \"#{base64_file_name}\"
73
+ $bytes = [System.Convert]::FromBase64String($base64_string)
74
+ $new_file = [System.IO.Path]::GetFullPath(\"#{command_file_name}\")
75
+ [System.IO.File]::WriteAllBytes($new_file,$bytes)
76
+ POWERSHELL
77
+ end
78
+
79
+ def unique_string
80
+ @unique_string ||= "#{Process.pid}-#{Time.now.to_i}"
81
+ end
82
+
83
+ # @return [String]
84
+ def get_file_path(file)
85
+ (winrm.run_cmd("echo %TEMP%\\#{file}"))[:data][0][:stdout].chomp
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -17,6 +17,14 @@ module Ridley
17
17
  attr_reader :options
18
18
  # @return [String]
19
19
  attr_reader :winrm_endpoint
20
+ # @return [CommandUploader]
21
+ attr_reader :command_uploader
22
+ # @return [Array]
23
+ attr_reader :command_uploaders
24
+
25
+ finalizer :finalizer
26
+
27
+ EMBEDDED_RUBY_PATH = 'C:\opscode\chef\embedded\bin\ruby'.freeze
20
28
 
21
29
  # @param host [String]
22
30
  # the host the worker is going to work on
@@ -25,24 +33,38 @@ module Ridley
25
33
  # * :password (String) the password for the user that will perform the bootstrap
26
34
  # * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
27
35
  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"
36
+ options = options.deep_symbolize_keys
37
+ @options = options[:winrm] || Hash.new
38
+ @host = host
39
+ @user = @options[:user]
40
+ @password = @options[:password]
41
+ @winrm_endpoint = "http://#{host}:#{winrm_port}/wsman"
42
+ @command_uploaders = Array.new
43
+ end
44
+
45
+ def finalizer
46
+ command_uploaders.map(&:cleanup)
34
47
  end
35
48
 
36
49
  def run(command)
37
- command = get_command(command)
50
+ command_uploaders << command_uploader = CommandUploader.new(winrm)
51
+ command = get_command(command, command_uploader)
38
52
 
39
53
  response = Ridley::HostConnector::Response.new(host)
40
54
  debug "Running WinRM Command: '#{command}' on: '#{host}' as: '#{user}'"
41
55
 
42
56
  output = winrm.run_cmd(command) do |stdout, stderr|
43
- response.stdout += stdout unless stdout.nil?
44
- response.stderr += stderr unless stderr.nil?
57
+ if stdout
58
+ response.stdout += stdout
59
+ info "NODE[#{host}] #{stdout}"
60
+ end
61
+
62
+ if stderr
63
+ response.stderr += stderr unless stderr.nil?
64
+ info "NODE[#{host}] #{stdout}"
65
+ end
45
66
  end
67
+
46
68
  response.exit_code = output[:exitcode]
47
69
 
48
70
  case response.exit_code
@@ -62,7 +84,7 @@ module Ridley
62
84
  [ :error, response ]
63
85
  end
64
86
 
65
- # @return [::WinRM::WinRMWebService]
87
+ # @return [WinRM::WinRMWebService]
66
88
  def winrm
67
89
  ::WinRM::WinRMWebService.new(winrm_endpoint, :plaintext, user: user, pass: password, disable_sspi: true, basic_auth_only: true)
68
90
  end
@@ -73,53 +95,51 @@ module Ridley
73
95
  end
74
96
 
75
97
  # 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.
98
+ # limit. Otherwise, we return an execution of the command as a batch file.
77
99
  #
78
100
  # @param command [String]
79
- #
101
+ #
80
102
  # @return [String]
81
- def get_command(command)
82
- if command.length < 2047
103
+ def get_command(command, command_uploader)
104
+ if command.length < CommandUploader::CHUNK_LIMIT
83
105
  command
84
106
  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)
107
+ debug "Detected a command that was longer than #{CommandUploader::CHUNK_LIMIT} characters, \
108
+ uploading command as a file to the host."
109
+ command_uploader.upload(command)
110
+ command_uploader.command
87
111
  end
88
112
  end
89
113
 
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
114
+ # Executes a chef-client run on the nodes
115
+ #
116
+ # @return [#run]
117
+ def chef_client
118
+ run("chef-client")
119
+ end
115
120
 
116
- "cmd.exe /C #{command_file_name}"
117
- end
121
+ # Executes a copy of the encrypted_data_bag_secret to the nodes
122
+ #
123
+ # @param [String] encrypted_data_bag_secret_path
124
+ # the path to the encrypted_data_bag_secret
125
+ #
126
+ # @return [#run]
127
+ def put_secret(encrypted_data_bag_secret_path)
128
+ secret = File.read(encrypted_data_bag_secret_path).chomp
129
+ command = "echo #{secret} > C:\\chef\\encrypted_data_bag_secret"
130
+ run(command)
131
+ end
118
132
 
119
- # @return [String]
120
- def get_file_path(file)
121
- (winrm.run_cmd("echo %TEMP%\\#{file}"))[:data][0][:stdout].chomp
122
- end
133
+ # Executes a provided Ruby script in the embedded Ruby installation
134
+ #
135
+ # @param [Array<String>] command_lines
136
+ # An Array of lines of the command to be executed
137
+ #
138
+ # @return [#run]
139
+ def ruby_script(command_lines)
140
+ command = "#{EMBEDDED_RUBY_PATH} -e \"#{command_lines.join(';')}\""
141
+ run(command)
142
+ end
123
143
  end
124
144
  end
125
145
  end