ridley 0.10.0.rc1 → 0.10.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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