chef-provisioning 2.0.1 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +906 -899
  3. data/Gemfile +17 -17
  4. data/LICENSE +201 -201
  5. data/README.md +312 -312
  6. data/Rakefile +55 -55
  7. data/chef-provisioning.gemspec +38 -38
  8. data/lib/chef/provider/load_balancer.rb +75 -75
  9. data/lib/chef/provider/machine.rb +219 -219
  10. data/lib/chef/provider/machine_batch.rb +224 -224
  11. data/lib/chef/provider/machine_execute.rb +36 -36
  12. data/lib/chef/provider/machine_file.rb +55 -55
  13. data/lib/chef/provider/machine_image.rb +105 -105
  14. data/lib/chef/provisioning.rb +110 -110
  15. data/lib/chef/provisioning/action_handler.rb +68 -68
  16. data/lib/chef/provisioning/add_prefix_action_handler.rb +35 -35
  17. data/lib/chef/provisioning/chef_managed_entry_store.rb +128 -128
  18. data/lib/chef/provisioning/chef_provider_action_handler.rb +74 -74
  19. data/lib/chef/provisioning/chef_run_data.rb +132 -132
  20. data/lib/chef/provisioning/convergence_strategy.rb +28 -28
  21. data/lib/chef/provisioning/convergence_strategy/ignore_convergence_failure.rb +54 -54
  22. data/lib/chef/provisioning/convergence_strategy/install_cached.rb +188 -188
  23. data/lib/chef/provisioning/convergence_strategy/install_msi.rb +71 -71
  24. data/lib/chef/provisioning/convergence_strategy/install_sh.rb +71 -71
  25. data/lib/chef/provisioning/convergence_strategy/no_converge.rb +35 -35
  26. data/lib/chef/provisioning/convergence_strategy/precreate_chef_objects.rb +255 -255
  27. data/lib/chef/provisioning/driver.rb +323 -323
  28. data/lib/chef/provisioning/load_balancer_spec.rb +14 -14
  29. data/lib/chef/provisioning/machine.rb +112 -112
  30. data/lib/chef/provisioning/machine/basic_machine.rb +84 -84
  31. data/lib/chef/provisioning/machine/unix_machine.rb +288 -288
  32. data/lib/chef/provisioning/machine/windows_machine.rb +108 -108
  33. data/lib/chef/provisioning/machine_image_spec.rb +34 -34
  34. data/lib/chef/provisioning/machine_spec.rb +58 -58
  35. data/lib/chef/provisioning/managed_entry.rb +121 -121
  36. data/lib/chef/provisioning/managed_entry_store.rb +136 -136
  37. data/lib/chef/provisioning/recipe_dsl.rb +99 -99
  38. data/lib/chef/provisioning/rspec.rb +27 -27
  39. data/lib/chef/provisioning/transport.rb +100 -100
  40. data/lib/chef/provisioning/transport/ssh.rb +403 -403
  41. data/lib/chef/provisioning/transport/winrm.rb +144 -144
  42. data/lib/chef/provisioning/version.rb +5 -5
  43. data/lib/chef/resource/chef_data_bag_resource.rb +146 -146
  44. data/lib/chef/resource/load_balancer.rb +57 -57
  45. data/lib/chef/resource/machine.rb +128 -128
  46. data/lib/chef/resource/machine_batch.rb +78 -78
  47. data/lib/chef/resource/machine_execute.rb +30 -30
  48. data/lib/chef/resource/machine_file.rb +34 -34
  49. data/lib/chef/resource/machine_image.rb +35 -35
  50. data/lib/chef_metal.rb +1 -1
  51. data/spec/chef/provisioning/convergence_strategy/ignore_convergence_failure_spec.rb +86 -86
  52. data/spec/spec_helper.rb +27 -27
  53. metadata +10 -4
@@ -1,27 +1,27 @@
1
- RSpec.shared_context "run with driver" do |driver_args|
2
- require 'cheffish/rspec/chef_run_support'
3
- extend Cheffish::RSpec::ChefRunSupport
4
-
5
- include_context "with a chef repo"
6
-
7
- driver_object = Chef::Provisioning.driver_for_url(driver_args[:driver_string])
8
-
9
- # globally set this as the driver. overridden by a resource's :driver attribute.
10
- before { Chef::Config.driver(driver_object) }
11
-
12
- let(:provisioning_driver) { driver_object }
13
-
14
- # only class methods are available outside of examples.
15
- def self.with_chef_server(description = "is running", *options, &block)
16
-
17
- # no need to repeat these every time.
18
- args = { organization: "spec_tests", server_scope: :context, port: 8900..9000 }
19
- args = args.merge(options.last) if options.last.is_a?(Hash)
20
-
21
- # this ends up in ChefZero::RSpec::RSpecClassMethods#when_the_chef_server, which defines all its code
22
- # inside an RSpec context and then runs `instance_eval` on &block--which means it's only available as a
23
- # block operator. it's not obviously impossible to factor out the code into a shared_context that we could
24
- # include as above with "with a chef repo", but that's a chef-zero patch.
25
- when_the_chef_12_server description, args, &block
26
- end
27
- end
1
+ RSpec.shared_context "run with driver" do |driver_args|
2
+ require 'cheffish/rspec/chef_run_support'
3
+ extend Cheffish::RSpec::ChefRunSupport
4
+
5
+ include_context "with a chef repo"
6
+
7
+ driver_object = Chef::Provisioning.driver_for_url(driver_args[:driver_string])
8
+
9
+ # globally set this as the driver. overridden by a resource's :driver attribute.
10
+ before { Chef::Config.driver(driver_object) }
11
+
12
+ let(:provisioning_driver) { driver_object }
13
+
14
+ # only class methods are available outside of examples.
15
+ def self.with_chef_server(description = "is running", *options, &block)
16
+
17
+ # no need to repeat these every time.
18
+ args = { organization: "spec_tests", server_scope: :context, port: 8900..9000 }
19
+ args = args.merge(options.last) if options.last.is_a?(Hash)
20
+
21
+ # this ends up in ChefZero::RSpec::RSpecClassMethods#when_the_chef_server, which defines all its code
22
+ # inside an RSpec context and then runs `instance_eval` on &block--which means it's only available as a
23
+ # block operator. it's not obviously impossible to factor out the code into a shared_context that we could
24
+ # include as above with "with a chef repo", but that's a chef-zero patch.
25
+ when_the_chef_12_server description, args, &block
26
+ end
27
+ end
@@ -1,100 +1,100 @@
1
- require 'timeout'
2
-
3
- class Chef
4
- module Provisioning
5
- class Transport
6
- DEFAULT_TIMEOUT = 15*60
7
-
8
- # Execute a program on the remote host.
9
- #
10
- # == Arguments
11
- # command: command to run. May be a shell-escaped string or a pre-split
12
- # array containing [PROGRAM, ARG1, ARG2, ...].
13
- # options: hash of options, including but not limited to:
14
- # :timeout => NUM_SECONDS - time to wait before program finishes
15
- # (throws an exception otherwise). Set to nil or 0 to
16
- # run with no timeout. Defaults to 15 minutes.
17
- # :stream => BOOLEAN - true to stream stdout and stderr to the console.
18
- # :stream => BLOCK - block to stream stdout and stderr to
19
- # (block.call(stdout_chunk, stderr_chunk))
20
- # :stream_stdout => FD - FD to stream stdout to (defaults to IO.stdout)
21
- # :stream_stderr => FD - FD to stream stderr to (defaults to IO.stderr)
22
- # :read_only => BOOLEAN - true if command is guaranteed not to
23
- # change system state (useful for Docker)
24
- def execute(command, options = {})
25
- raise "execute not overridden on #{self.class}"
26
- end
27
-
28
- # TODO: make exceptions for these instead of just returning nil / silently failing
29
- def read_file(path)
30
- raise "read_file not overridden on #{self.class}"
31
- end
32
-
33
- def write_file(path, content)
34
- raise "write_file not overridden on #{self.class}"
35
- end
36
-
37
- def download_file(path, local_path)
38
- IO.write(local_path, read_file(path))
39
- end
40
-
41
- def upload_file(local_path, path)
42
- write_file(path, IO.read(local_path))
43
- end
44
-
45
- def make_url_available_to_remote(local_url)
46
- raise "make_url_available_to_remote not overridden on #{self.class}"
47
- end
48
-
49
- def disconnect
50
- raise "disconnect not overridden on #{self.class}"
51
- end
52
-
53
- def available?
54
- raise "available? not overridden on #{self.class}"
55
- end
56
-
57
- # Config hash, including :log_level and :logger as keys
58
- def config
59
- raise "config not overridden on #{self.class}"
60
- end
61
-
62
- protected
63
-
64
- # Helper to implement stdout/stderr streaming in execute
65
- def stream_chunk(options, stdout_chunk, stderr_chunk)
66
- if options[:stream].is_a?(Proc)
67
- options[:stream].call(stdout_chunk, stderr_chunk)
68
- else
69
- if stdout_chunk
70
- if options.has_key?(:stream_stdout)
71
- stream = options[:stream_stdout]
72
- elsif options[:stream] || config[:log_level] == :debug
73
- stream = config[:stdout] || STDOUT
74
- end
75
-
76
- stream.print stdout_chunk if stream
77
- end
78
-
79
- if stderr_chunk
80
- if options.has_key?(:stream_stderr)
81
- stream = options[:stream_stderr]
82
- elsif options[:stream] || config[:log_level] == :debug
83
- stream = config[:stderr] || STDERR
84
- end
85
-
86
- stream.print stderr_chunk if stream
87
- end
88
- end
89
- end
90
-
91
- def with_execute_timeout(options, &block)
92
- Timeout::timeout(execute_timeout(options), &block)
93
- end
94
-
95
- def execute_timeout(options)
96
- options.has_key?(:timeout) ? options[:timeout] : DEFAULT_TIMEOUT
97
- end
98
- end
99
- end
100
- end
1
+ require 'timeout'
2
+
3
+ class Chef
4
+ module Provisioning
5
+ class Transport
6
+ DEFAULT_TIMEOUT = 15*60
7
+
8
+ # Execute a program on the remote host.
9
+ #
10
+ # == Arguments
11
+ # command: command to run. May be a shell-escaped string or a pre-split
12
+ # array containing [PROGRAM, ARG1, ARG2, ...].
13
+ # options: hash of options, including but not limited to:
14
+ # :timeout => NUM_SECONDS - time to wait before program finishes
15
+ # (throws an exception otherwise). Set to nil or 0 to
16
+ # run with no timeout. Defaults to 15 minutes.
17
+ # :stream => BOOLEAN - true to stream stdout and stderr to the console.
18
+ # :stream => BLOCK - block to stream stdout and stderr to
19
+ # (block.call(stdout_chunk, stderr_chunk))
20
+ # :stream_stdout => FD - FD to stream stdout to (defaults to IO.stdout)
21
+ # :stream_stderr => FD - FD to stream stderr to (defaults to IO.stderr)
22
+ # :read_only => BOOLEAN - true if command is guaranteed not to
23
+ # change system state (useful for Docker)
24
+ def execute(command, options = {})
25
+ raise "execute not overridden on #{self.class}"
26
+ end
27
+
28
+ # TODO: make exceptions for these instead of just returning nil / silently failing
29
+ def read_file(path)
30
+ raise "read_file not overridden on #{self.class}"
31
+ end
32
+
33
+ def write_file(path, content)
34
+ raise "write_file not overridden on #{self.class}"
35
+ end
36
+
37
+ def download_file(path, local_path)
38
+ IO.write(local_path, read_file(path))
39
+ end
40
+
41
+ def upload_file(local_path, path)
42
+ write_file(path, IO.read(local_path))
43
+ end
44
+
45
+ def make_url_available_to_remote(local_url)
46
+ raise "make_url_available_to_remote not overridden on #{self.class}"
47
+ end
48
+
49
+ def disconnect
50
+ raise "disconnect not overridden on #{self.class}"
51
+ end
52
+
53
+ def available?
54
+ raise "available? not overridden on #{self.class}"
55
+ end
56
+
57
+ # Config hash, including :log_level and :logger as keys
58
+ def config
59
+ raise "config not overridden on #{self.class}"
60
+ end
61
+
62
+ protected
63
+
64
+ # Helper to implement stdout/stderr streaming in execute
65
+ def stream_chunk(options, stdout_chunk, stderr_chunk)
66
+ if options[:stream].is_a?(Proc)
67
+ options[:stream].call(stdout_chunk, stderr_chunk)
68
+ else
69
+ if stdout_chunk
70
+ if options.has_key?(:stream_stdout)
71
+ stream = options[:stream_stdout]
72
+ elsif options[:stream] || config[:log_level] == :debug
73
+ stream = config[:stdout] || STDOUT
74
+ end
75
+
76
+ stream.print stdout_chunk if stream
77
+ end
78
+
79
+ if stderr_chunk
80
+ if options.has_key?(:stream_stderr)
81
+ stream = options[:stream_stderr]
82
+ elsif options[:stream] || config[:log_level] == :debug
83
+ stream = config[:stderr] || STDERR
84
+ end
85
+
86
+ stream.print stderr_chunk if stream
87
+ end
88
+ end
89
+ end
90
+
91
+ def with_execute_timeout(options, &block)
92
+ Timeout::timeout(execute_timeout(options), &block)
93
+ end
94
+
95
+ def execute_timeout(options)
96
+ options.has_key?(:timeout) ? options[:timeout] : DEFAULT_TIMEOUT
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,403 +1,403 @@
1
- require 'chef/provisioning/transport'
2
- require 'chef/log'
3
- require 'uri'
4
- require 'socket'
5
- require 'timeout'
6
- require 'net/ssh'
7
- require 'net/scp'
8
- require 'net/ssh/gateway'
9
-
10
- class Chef
11
- module Provisioning
12
- class Transport
13
- class SSH < Chef::Provisioning::Transport
14
- #
15
- # Create a new SSH transport.
16
- #
17
- # == Arguments
18
- #
19
- # - host: the host to connect to, e.g. '145.14.51.45'
20
- # - username: the username to connect with
21
- # - ssh_options: a list of options to Net::SSH.start
22
- # - options: a hash of options for the transport itself, including:
23
- # - :prefix: a prefix to send before each command (e.g. "sudo ")
24
- # - :ssh_pty_enable: set to false to disable pty (some instances don't
25
- # support this, most do)
26
- # - :ssh_gateway: the gateway to use, e.g. "jkeiser@145.14.51.45:222".
27
- # nil (the default) means no gateway. If the username is omitted,
28
- # then the default username is used instead (i.e. the user running
29
- # chef, or the username configured in .ssh/config).
30
- # - :scp_temp_dir: a directory to use as the temporary location for
31
- # files that are copied to the host via SCP.
32
- # Only used if :prefix is set. Default is '/tmp' if unspecified.
33
- # - global_config: an options hash that looks suspiciously similar to
34
- # Chef::Config, containing at least the key :log_level.
35
- #
36
- # The options are used in
37
- # Net::SSH.start(host, username, ssh_options)
38
-
39
- def initialize(host, username, ssh_options, options, global_config)
40
- @host = host
41
- @username = username
42
- @ssh_options = ssh_options
43
- @options = options
44
- @config = global_config
45
- @remote_forwards = ssh_options.delete(:remote_forwards) { Array.new }
46
- end
47
-
48
- attr_reader :host
49
- attr_reader :username
50
- attr_reader :ssh_options
51
- attr_reader :options
52
- attr_reader :config
53
-
54
- def execute(command, execute_options = {})
55
- Chef::Log.info("#{self.object_id} Executing #{options[:prefix]}#{command} on #{username}@#{host}")
56
- stdout = ''
57
- stderr = ''
58
- exitstatus = nil
59
- session # grab session outside timeout, it has its own timeout
60
-
61
- with_execute_timeout(execute_options) do
62
- @remote_forwards.each do |forward_info|
63
- # -R flag to openssh client allows optional :remote_host and
64
- # requires the other values so let's do that too.
65
- remote_host = forward_info.fetch(:remote_host, 'localhost')
66
- remote_port = forward_info.fetch(:remote_port)
67
- local_host = forward_info.fetch(:local_host)
68
- local_port = forward_info.fetch(:local_port)
69
-
70
- actual_port, actual_host = forward_port(local_port, local_host, remote_port, remote_host)
71
- Chef::Log.info("#{host} forwarded remote #{actual_host}:#{actual_port} to local #{local_host}:#{local_port}")
72
- end
73
-
74
- channel = session.open_channel do |channel|
75
- # Enable PTY unless otherwise specified, some instances require this
76
- unless options[:ssh_pty_enable] == false
77
- channel.request_pty do |chan, success|
78
- raise "could not get pty" if !success && options[:ssh_pty_enable]
79
- end
80
- end
81
-
82
- channel.exec("#{options[:prefix]}#{command}") do |ch, success|
83
- raise "could not execute command: #{command.inspect}" unless success
84
-
85
- channel.on_data do |ch2, data|
86
- stdout << data
87
- stream_chunk(execute_options, data, nil)
88
- end
89
-
90
- channel.on_extended_data do |ch2, type, data|
91
- stderr << data
92
- stream_chunk(execute_options, nil, data)
93
- end
94
-
95
- channel.on_request "exit-status" do |ch, data|
96
- exitstatus = data.read_long
97
- end
98
- end
99
- end
100
-
101
- channel.wait
102
-
103
- @remote_forwards.each do |forward_info|
104
- # -R flag to openssh client allows optional :remote_host and
105
- # requires the other values so let's do that too.
106
- remote_host = forward_info.fetch(:remote_host, 'localhost')
107
- remote_port = forward_info.fetch(:remote_port)
108
- local_host = forward_info.fetch(:local_host)
109
- local_port = forward_info.fetch(:local_port)
110
-
111
- session.forward.cancel_remote(remote_port, remote_host)
112
- session.loop { session.forward.active_remotes.include?([remote_port, remote_host]) }
113
-
114
- Chef::Log.info("#{host} canceled remote forward #{remote_host}:#{remote_port}")
115
- end
116
- end
117
-
118
- Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
119
- Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != '' && !options[:stream] && !options[:stream_stdout] && config[:log_level] != :debug
120
- Chef::Log.info("Stderr was:\n#{stderr}") if stderr != '' && !options[:stream] && !options[:stream_stderr] && config[:log_level] != :debug
121
- SSHResult.new(command, execute_options, stdout, stderr, exitstatus)
122
- end
123
-
124
- # TODO why does #read_file download it to the target host?
125
- def read_file(path)
126
- Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
127
- result = StringIO.new
128
- download(path, result)
129
- result.string
130
- end
131
-
132
- def download_file(path, local_path)
133
- Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
134
- download(path, local_path)
135
- end
136
-
137
- def remote_tempfile(path)
138
- File.join(scp_temp_dir, "#{File.basename(path)}.#{Random.rand(2**32)}")
139
- end
140
-
141
- def write_file(path, content)
142
- execute("mkdir -p #{File.dirname(path)}").error!
143
- if options[:prefix]
144
- # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
145
- tempfile = remote_tempfile(path)
146
- Chef::Log.debug("Writing #{content.length} bytes to #{tempfile} on #{username}@#{host}")
147
- Net::SCP.new(session).upload!(StringIO.new(content), tempfile)
148
- execute("mv #{tempfile} #{path}").error!
149
- else
150
- Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
151
- Net::SCP.new(session).upload!(StringIO.new(content), path)
152
- end
153
- end
154
-
155
- def upload_file(local_path, path)
156
- execute("mkdir -p #{File.dirname(path)}").error!
157
- if options[:prefix]
158
- # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
159
- tempfile = remote_tempfile(path)
160
- Chef::Log.debug("Uploading #{local_path} to #{tempfile} on #{username}@#{host}")
161
- Net::SCP.new(session).upload!(local_path, tempfile)
162
- begin
163
- execute("mv #{tempfile} #{path}").error!
164
- rescue
165
- # Clean up if we were unable to move
166
- execute("rm #{tempfile}").error!
167
- end
168
- else
169
- Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
170
- Net::SCP.new(session).upload!(local_path, path)
171
- end
172
- end
173
-
174
- def make_url_available_to_remote(local_url)
175
- uri = URI(local_url)
176
- if is_local_machine(uri.host)
177
- port, host = forward_port(uri.port, uri.host, uri.port, 'localhost')
178
- if !port
179
- # Try harder if the port is already taken
180
- port, host = forward_port(uri.port, uri.host, 0, 'localhost')
181
- if !port
182
- raise "Error forwarding port: could not forward #{uri.port} or 0"
183
- end
184
- end
185
- uri.host = host
186
- uri.port = port
187
- Chef::Log.info("Port forwarded: local URL #{local_url} is available to #{self.host} as #{uri.to_s} for the duration of this SSH connection.")
188
- else
189
- Chef::Log.info("#{host} not forwarding non-local #{local_url}")
190
- end
191
- uri.to_s
192
- end
193
-
194
- def disconnect
195
- if @session
196
- begin
197
- Chef::Log.info("Closing SSH session on #{username}@#{host}")
198
- @session.close
199
- rescue
200
- ensure
201
- @session = nil
202
- end
203
- end
204
- end
205
-
206
- def available?
207
- timeout = ssh_options[:timeout] || 10
208
- execute('pwd', :timeout => timeout)
209
- true
210
- rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EHOSTDOWN, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::Disconnect, Net::SSH::ConnectionTimeout
211
- Chef::Log.debug("#{username}@#{host} unavailable: network connection failed or broke: #{$!.inspect}")
212
- disconnect
213
- false
214
- rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch
215
- Chef::Log.debug("#{username}@#{host} unavailable: SSH authentication error: #{$!.inspect} ")
216
- disconnect
217
- false
218
- end
219
-
220
- protected
221
-
222
- def session
223
- @session ||= begin
224
- # Small initial connection timeout (10s) to help us fail faster when server is just dead
225
- ssh_start_opts = { timeout:10 }.merge(ssh_options)
226
- Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_start_opts.dup.tap {
227
- |ssh| ssh.delete(:key_data) }.inspect}")
228
- begin
229
- if gateway? then gateway.ssh(host, username, ssh_start_opts)
230
- else Net::SSH.start(host, username, ssh_start_opts)
231
- end
232
- rescue Timeout::Error, Net::SSH::ConnectionTimeout
233
- Chef::Log.debug("Timed out connecting to SSH: #{$!}")
234
- raise InitialConnectTimeout.new($!)
235
- end
236
- end
237
- end
238
-
239
- def download(path, local_path)
240
- if options[:prefix]
241
- # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
242
- tempfile = remote_tempfile(path)
243
- Chef::Log.debug("Downloading #{path} from #{tempfile} to #{local_path} on #{username}@#{host}")
244
- begin
245
- execute("cp #{path} #{tempfile}").error!
246
- execute("chown #{username} #{tempfile}").error!
247
- do_download tempfile, local_path
248
- rescue => e
249
- Chef::Log.error "Unable to download #{path} to #{tempfile} on #{username}@#{host} -- #{e}"
250
- nil
251
- ensure
252
- # Clean up afterwards
253
- begin
254
- execute("rm #{tempfile}").error!
255
- rescue => e
256
- Chef::Log.warn "Unable to clean up #{tempfile} on #{username}@#{host} -- #{e}"
257
- end
258
- end
259
- else
260
- do_download path, local_path
261
- end
262
- end
263
-
264
- def do_download(path, local_path)
265
- channel = Net::SCP.new(session).download(path, local_path)
266
- begin
267
- channel.wait
268
- Chef::Log.debug "SCP completed for: #{path} to #{local_path}"
269
- rescue Net::SCP::Error => e
270
- Chef::Log.error "Error with SCP: #{e}"
271
- # TODO we need a way to distinguish between "directory or file does not exist" and "SCP did not finish successfully"
272
- nil
273
- ensure
274
- # ensure the channel is closed
275
- channel.close
276
- channel.wait
277
- end
278
-
279
- nil
280
- end
281
-
282
- class SSHResult
283
- def initialize(command, options, stdout, stderr, exitstatus)
284
- @command = command
285
- @options = options
286
- @stdout = stdout
287
- @stderr = stderr
288
- @exitstatus = exitstatus
289
- end
290
-
291
- attr_reader :command
292
- attr_reader :options
293
- attr_reader :stdout
294
- attr_reader :stderr
295
- attr_reader :exitstatus
296
-
297
- def error!
298
- if exitstatus != 0
299
- # TODO stdout/stderr is already printed at info/debug level. Let's not print it twice, it's a lot.
300
- msg = "Error: command '#{command}' exited with code #{exitstatus}.\n"
301
- raise msg
302
- end
303
- end
304
- end
305
-
306
- class InitialConnectTimeout < Timeout::Error
307
- def initialize(original_error)
308
- super(original_error.message)
309
- @original_error = original_error
310
- end
311
-
312
- attr_reader :original_error
313
- end
314
-
315
- private
316
-
317
- def scp_temp_dir
318
- @scp_temp_dir ||= options.fetch(:scp_temp_dir, '/tmp')
319
- end
320
-
321
- def gateway?
322
- options.key?(:ssh_gateway) and ! options[:ssh_gateway].nil?
323
- end
324
-
325
- def gateway
326
- gw_user, gw_host = options[:ssh_gateway].split('@')
327
- # If we didn't have an '@' in the above, then the value is actually
328
- # the hostname, not the username.
329
- gw_host, gw_user = gw_user, gw_host if gw_host.nil?
330
- gw_host, gw_port = gw_host.split(':')
331
-
332
- ssh_start_opts = { timeout:10 }.merge(ssh_options)
333
- ssh_start_opts[:port] = gw_port || 22
334
-
335
- Chef::Log.debug("Opening SSH gateway to #{gw_user}@#{gw_host} with options #{ssh_start_opts.dup.tap {
336
- |ssh| ssh.delete(:key_data) }.inspect}")
337
- begin
338
- Net::SSH::Gateway.new(gw_host, gw_user, ssh_start_opts)
339
- rescue Errno::ETIMEDOUT
340
- Chef::Log.debug("Timed out connecting to gateway: #{$!}")
341
- raise InitialConnectTimeout.new($!)
342
- end
343
- end
344
-
345
- def is_local_machine(host)
346
- local_addrs = Socket.ip_address_list
347
- host_addrs = Addrinfo.getaddrinfo(host, nil)
348
- local_addrs.any? do |local_addr|
349
- host_addrs.any? do |host_addr|
350
- local_addr.ip_address == host_addr.ip_address
351
- end
352
- end
353
- end
354
-
355
- # Forwards a port over the connection, and returns the
356
- def forward_port(local_port, local_host, remote_port, remote_host)
357
- # This bit is from the documentation.
358
- if session.forward.respond_to?(:active_remote_destinations)
359
- # active_remote_destinations tells us exactly what remotes the current
360
- # ssh session is *actually* tracking. If multiple people share this
361
- # session and set up their own remotes, this will prevent us from
362
- # overwriting them.
363
-
364
- actual_remote_port, actual_remote_host = session.forward.active_remote_destinations[[local_port, local_host]]
365
- if !actual_remote_port
366
- Chef::Log.info("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
367
-
368
- session.forward.remote(local_port, local_host, remote_port, remote_host) do |new_remote_port, new_remote_host|
369
- actual_remote_host = new_remote_host
370
- actual_remote_port = new_remote_port || :error
371
- :no_exception # I'll take care of it myself, thanks
372
- end
373
- # Kick SSH until we get a response
374
- session.loop { !actual_remote_port }
375
- if actual_remote_port == :error
376
- return nil
377
- end
378
- end
379
- [ actual_remote_port, actual_remote_host ]
380
- else
381
- # If active_remote_destinations isn't on net-ssh, we stash our own list
382
- # of ports *we* have forwarded on the connection, and hope that we are
383
- # right.
384
- # TODO let's remove this when net-ssh 2.9.2 is old enough, and
385
- # bump the required net-ssh version.
386
-
387
- @forwarded_ports ||= {}
388
- remote_port, remote_host = @forwarded_ports[[local_port, local_host]]
389
- if !remote_port
390
- Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
391
- old_active_remotes = session.forward.active_remotes
392
- session.forward.remote(local_port, local_host, local_port)
393
- session.loop { !(session.forward.active_remotes.length > old_active_remotes.length) }
394
- remote_port, remote_host = (session.forward.active_remotes - old_active_remotes).first
395
- @forwarded_ports[[local_port, local_host]] = [ remote_port, remote_host ]
396
- end
397
- [ remote_port, remote_host ]
398
- end
399
- end
400
- end
401
- end
402
- end
403
- end
1
+ require 'chef/provisioning/transport'
2
+ require 'chef/log'
3
+ require 'uri'
4
+ require 'socket'
5
+ require 'timeout'
6
+ require 'net/ssh'
7
+ require 'net/scp'
8
+ require 'net/ssh/gateway'
9
+
10
+ class Chef
11
+ module Provisioning
12
+ class Transport
13
+ class SSH < Chef::Provisioning::Transport
14
+ #
15
+ # Create a new SSH transport.
16
+ #
17
+ # == Arguments
18
+ #
19
+ # - host: the host to connect to, e.g. '145.14.51.45'
20
+ # - username: the username to connect with
21
+ # - ssh_options: a list of options to Net::SSH.start
22
+ # - options: a hash of options for the transport itself, including:
23
+ # - :prefix: a prefix to send before each command (e.g. "sudo ")
24
+ # - :ssh_pty_enable: set to false to disable pty (some instances don't
25
+ # support this, most do)
26
+ # - :ssh_gateway: the gateway to use, e.g. "jkeiser@145.14.51.45:222".
27
+ # nil (the default) means no gateway. If the username is omitted,
28
+ # then the default username is used instead (i.e. the user running
29
+ # chef, or the username configured in .ssh/config).
30
+ # - :scp_temp_dir: a directory to use as the temporary location for
31
+ # files that are copied to the host via SCP.
32
+ # Only used if :prefix is set. Default is '/tmp' if unspecified.
33
+ # - global_config: an options hash that looks suspiciously similar to
34
+ # Chef::Config, containing at least the key :log_level.
35
+ #
36
+ # The options are used in
37
+ # Net::SSH.start(host, username, ssh_options)
38
+
39
+ def initialize(host, username, ssh_options, options, global_config)
40
+ @host = host
41
+ @username = username
42
+ @ssh_options = ssh_options
43
+ @options = options
44
+ @config = global_config
45
+ @remote_forwards = ssh_options.delete(:remote_forwards) { Array.new }
46
+ end
47
+
48
+ attr_reader :host
49
+ attr_reader :username
50
+ attr_reader :ssh_options
51
+ attr_reader :options
52
+ attr_reader :config
53
+
54
+ def execute(command, execute_options = {})
55
+ Chef::Log.info("#{self.object_id} Executing #{options[:prefix]}#{command} on #{username}@#{host}")
56
+ stdout = ''
57
+ stderr = ''
58
+ exitstatus = nil
59
+ session # grab session outside timeout, it has its own timeout
60
+
61
+ with_execute_timeout(execute_options) do
62
+ @remote_forwards.each do |forward_info|
63
+ # -R flag to openssh client allows optional :remote_host and
64
+ # requires the other values so let's do that too.
65
+ remote_host = forward_info.fetch(:remote_host, 'localhost')
66
+ remote_port = forward_info.fetch(:remote_port)
67
+ local_host = forward_info.fetch(:local_host)
68
+ local_port = forward_info.fetch(:local_port)
69
+
70
+ actual_port, actual_host = forward_port(local_port, local_host, remote_port, remote_host)
71
+ Chef::Log.info("#{host} forwarded remote #{actual_host}:#{actual_port} to local #{local_host}:#{local_port}")
72
+ end
73
+
74
+ channel = session.open_channel do |channel|
75
+ # Enable PTY unless otherwise specified, some instances require this
76
+ unless options[:ssh_pty_enable] == false
77
+ channel.request_pty do |chan, success|
78
+ raise "could not get pty" if !success && options[:ssh_pty_enable]
79
+ end
80
+ end
81
+
82
+ channel.exec("#{options[:prefix]}#{command}") do |ch, success|
83
+ raise "could not execute command: #{command.inspect}" unless success
84
+
85
+ channel.on_data do |ch2, data|
86
+ stdout << data
87
+ stream_chunk(execute_options, data, nil)
88
+ end
89
+
90
+ channel.on_extended_data do |ch2, type, data|
91
+ stderr << data
92
+ stream_chunk(execute_options, nil, data)
93
+ end
94
+
95
+ channel.on_request "exit-status" do |ch, data|
96
+ exitstatus = data.read_long
97
+ end
98
+ end
99
+ end
100
+
101
+ channel.wait
102
+
103
+ @remote_forwards.each do |forward_info|
104
+ # -R flag to openssh client allows optional :remote_host and
105
+ # requires the other values so let's do that too.
106
+ remote_host = forward_info.fetch(:remote_host, 'localhost')
107
+ remote_port = forward_info.fetch(:remote_port)
108
+ local_host = forward_info.fetch(:local_host)
109
+ local_port = forward_info.fetch(:local_port)
110
+
111
+ session.forward.cancel_remote(remote_port, remote_host)
112
+ session.loop { session.forward.active_remotes.include?([remote_port, remote_host]) }
113
+
114
+ Chef::Log.info("#{host} canceled remote forward #{remote_host}:#{remote_port}")
115
+ end
116
+ end
117
+
118
+ Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
119
+ Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != '' && !options[:stream] && !options[:stream_stdout] && config[:log_level] != :debug
120
+ Chef::Log.info("Stderr was:\n#{stderr}") if stderr != '' && !options[:stream] && !options[:stream_stderr] && config[:log_level] != :debug
121
+ SSHResult.new(command, execute_options, stdout, stderr, exitstatus)
122
+ end
123
+
124
+ # TODO why does #read_file download it to the target host?
125
+ def read_file(path)
126
+ Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
127
+ result = StringIO.new
128
+ download(path, result)
129
+ result.string
130
+ end
131
+
132
+ def download_file(path, local_path)
133
+ Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
134
+ download(path, local_path)
135
+ end
136
+
137
+ def remote_tempfile(path)
138
+ File.join(scp_temp_dir, "#{File.basename(path)}.#{Random.rand(2**32)}")
139
+ end
140
+
141
+ def write_file(path, content)
142
+ execute("mkdir -p #{File.dirname(path)}").error!
143
+ if options[:prefix]
144
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
145
+ tempfile = remote_tempfile(path)
146
+ Chef::Log.debug("Writing #{content.length} bytes to #{tempfile} on #{username}@#{host}")
147
+ Net::SCP.new(session).upload!(StringIO.new(content), tempfile)
148
+ execute("mv #{tempfile} #{path}").error!
149
+ else
150
+ Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
151
+ Net::SCP.new(session).upload!(StringIO.new(content), path)
152
+ end
153
+ end
154
+
155
+ def upload_file(local_path, path)
156
+ execute("mkdir -p #{File.dirname(path)}").error!
157
+ if options[:prefix]
158
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
159
+ tempfile = remote_tempfile(path)
160
+ Chef::Log.debug("Uploading #{local_path} to #{tempfile} on #{username}@#{host}")
161
+ Net::SCP.new(session).upload!(local_path, tempfile)
162
+ begin
163
+ execute("mv #{tempfile} #{path}").error!
164
+ rescue
165
+ # Clean up if we were unable to move
166
+ execute("rm #{tempfile}").error!
167
+ end
168
+ else
169
+ Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
170
+ Net::SCP.new(session).upload!(local_path, path)
171
+ end
172
+ end
173
+
174
+ def make_url_available_to_remote(local_url)
175
+ uri = URI(local_url)
176
+ if is_local_machine(uri.host)
177
+ port, host = forward_port(uri.port, uri.host, uri.port, 'localhost')
178
+ if !port
179
+ # Try harder if the port is already taken
180
+ port, host = forward_port(uri.port, uri.host, 0, 'localhost')
181
+ if !port
182
+ raise "Error forwarding port: could not forward #{uri.port} or 0"
183
+ end
184
+ end
185
+ uri.host = host
186
+ uri.port = port
187
+ Chef::Log.info("Port forwarded: local URL #{local_url} is available to #{self.host} as #{uri.to_s} for the duration of this SSH connection.")
188
+ else
189
+ Chef::Log.info("#{host} not forwarding non-local #{local_url}")
190
+ end
191
+ uri.to_s
192
+ end
193
+
194
+ def disconnect
195
+ if @session
196
+ begin
197
+ Chef::Log.info("Closing SSH session on #{username}@#{host}")
198
+ @session.close
199
+ rescue
200
+ ensure
201
+ @session = nil
202
+ end
203
+ end
204
+ end
205
+
206
+ def available?
207
+ timeout = ssh_options[:timeout] || 10
208
+ execute('pwd', :timeout => timeout)
209
+ true
210
+ rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EHOSTDOWN, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::Disconnect, Net::SSH::ConnectionTimeout
211
+ Chef::Log.debug("#{username}@#{host} unavailable: network connection failed or broke: #{$!.inspect}")
212
+ disconnect
213
+ false
214
+ rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch
215
+ Chef::Log.debug("#{username}@#{host} unavailable: SSH authentication error: #{$!.inspect} ")
216
+ disconnect
217
+ false
218
+ end
219
+
220
+ protected
221
+
222
+ def session
223
+ @session ||= begin
224
+ # Small initial connection timeout (10s) to help us fail faster when server is just dead
225
+ ssh_start_opts = { timeout:10 }.merge(ssh_options)
226
+ Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_start_opts.dup.tap {
227
+ |ssh| ssh.delete(:key_data) }.inspect}")
228
+ begin
229
+ if gateway? then gateway.ssh(host, username, ssh_start_opts)
230
+ else Net::SSH.start(host, username, ssh_start_opts)
231
+ end
232
+ rescue Timeout::Error, Net::SSH::ConnectionTimeout
233
+ Chef::Log.debug("Timed out connecting to SSH: #{$!}")
234
+ raise InitialConnectTimeout.new($!)
235
+ end
236
+ end
237
+ end
238
+
239
+ def download(path, local_path)
240
+ if options[:prefix]
241
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
242
+ tempfile = remote_tempfile(path)
243
+ Chef::Log.debug("Downloading #{path} from #{tempfile} to #{local_path} on #{username}@#{host}")
244
+ begin
245
+ execute("cp #{path} #{tempfile}").error!
246
+ execute("chown #{username} #{tempfile}").error!
247
+ do_download tempfile, local_path
248
+ rescue => e
249
+ Chef::Log.error "Unable to download #{path} to #{tempfile} on #{username}@#{host} -- #{e}"
250
+ nil
251
+ ensure
252
+ # Clean up afterwards
253
+ begin
254
+ execute("rm #{tempfile}").error!
255
+ rescue => e
256
+ Chef::Log.warn "Unable to clean up #{tempfile} on #{username}@#{host} -- #{e}"
257
+ end
258
+ end
259
+ else
260
+ do_download path, local_path
261
+ end
262
+ end
263
+
264
+ def do_download(path, local_path)
265
+ channel = Net::SCP.new(session).download(path, local_path)
266
+ begin
267
+ channel.wait
268
+ Chef::Log.debug "SCP completed for: #{path} to #{local_path}"
269
+ rescue Net::SCP::Error => e
270
+ Chef::Log.error "Error with SCP: #{e}"
271
+ # TODO we need a way to distinguish between "directory or file does not exist" and "SCP did not finish successfully"
272
+ nil
273
+ ensure
274
+ # ensure the channel is closed
275
+ channel.close
276
+ channel.wait
277
+ end
278
+
279
+ nil
280
+ end
281
+
282
+ class SSHResult
283
+ def initialize(command, options, stdout, stderr, exitstatus)
284
+ @command = command
285
+ @options = options
286
+ @stdout = stdout
287
+ @stderr = stderr
288
+ @exitstatus = exitstatus
289
+ end
290
+
291
+ attr_reader :command
292
+ attr_reader :options
293
+ attr_reader :stdout
294
+ attr_reader :stderr
295
+ attr_reader :exitstatus
296
+
297
+ def error!
298
+ if exitstatus != 0
299
+ # TODO stdout/stderr is already printed at info/debug level. Let's not print it twice, it's a lot.
300
+ msg = "Error: command '#{command}' exited with code #{exitstatus}.\n"
301
+ raise msg
302
+ end
303
+ end
304
+ end
305
+
306
+ class InitialConnectTimeout < Timeout::Error
307
+ def initialize(original_error)
308
+ super(original_error.message)
309
+ @original_error = original_error
310
+ end
311
+
312
+ attr_reader :original_error
313
+ end
314
+
315
+ private
316
+
317
+ def scp_temp_dir
318
+ @scp_temp_dir ||= options.fetch(:scp_temp_dir, '/tmp')
319
+ end
320
+
321
+ def gateway?
322
+ options.key?(:ssh_gateway) and ! options[:ssh_gateway].nil?
323
+ end
324
+
325
+ def gateway
326
+ gw_user, gw_host = options[:ssh_gateway].split('@')
327
+ # If we didn't have an '@' in the above, then the value is actually
328
+ # the hostname, not the username.
329
+ gw_host, gw_user = gw_user, gw_host if gw_host.nil?
330
+ gw_host, gw_port = gw_host.split(':')
331
+
332
+ ssh_start_opts = { timeout:10 }.merge(ssh_options)
333
+ ssh_start_opts[:port] = gw_port || 22
334
+
335
+ Chef::Log.debug("Opening SSH gateway to #{gw_user}@#{gw_host} with options #{ssh_start_opts.dup.tap {
336
+ |ssh| ssh.delete(:key_data) }.inspect}")
337
+ begin
338
+ Net::SSH::Gateway.new(gw_host, gw_user, ssh_start_opts)
339
+ rescue Errno::ETIMEDOUT
340
+ Chef::Log.debug("Timed out connecting to gateway: #{$!}")
341
+ raise InitialConnectTimeout.new($!)
342
+ end
343
+ end
344
+
345
+ def is_local_machine(host)
346
+ local_addrs = Socket.ip_address_list
347
+ host_addrs = Addrinfo.getaddrinfo(host, nil)
348
+ local_addrs.any? do |local_addr|
349
+ host_addrs.any? do |host_addr|
350
+ local_addr.ip_address == host_addr.ip_address
351
+ end
352
+ end
353
+ end
354
+
355
+ # Forwards a port over the connection, and returns the
356
+ def forward_port(local_port, local_host, remote_port, remote_host)
357
+ # This bit is from the documentation.
358
+ if session.forward.respond_to?(:active_remote_destinations)
359
+ # active_remote_destinations tells us exactly what remotes the current
360
+ # ssh session is *actually* tracking. If multiple people share this
361
+ # session and set up their own remotes, this will prevent us from
362
+ # overwriting them.
363
+
364
+ actual_remote_port, actual_remote_host = session.forward.active_remote_destinations[[local_port, local_host]]
365
+ if !actual_remote_port
366
+ Chef::Log.info("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
367
+
368
+ session.forward.remote(local_port, local_host, remote_port, remote_host) do |new_remote_port, new_remote_host|
369
+ actual_remote_host = new_remote_host
370
+ actual_remote_port = new_remote_port || :error
371
+ :no_exception # I'll take care of it myself, thanks
372
+ end
373
+ # Kick SSH until we get a response
374
+ session.loop { !actual_remote_port }
375
+ if actual_remote_port == :error
376
+ return nil
377
+ end
378
+ end
379
+ [ actual_remote_port, actual_remote_host ]
380
+ else
381
+ # If active_remote_destinations isn't on net-ssh, we stash our own list
382
+ # of ports *we* have forwarded on the connection, and hope that we are
383
+ # right.
384
+ # TODO let's remove this when net-ssh 2.9.2 is old enough, and
385
+ # bump the required net-ssh version.
386
+
387
+ @forwarded_ports ||= {}
388
+ remote_port, remote_host = @forwarded_ports[[local_port, local_host]]
389
+ if !remote_port
390
+ Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
391
+ old_active_remotes = session.forward.active_remotes
392
+ session.forward.remote(local_port, local_host, local_port)
393
+ session.loop { !(session.forward.active_remotes.length > old_active_remotes.length) }
394
+ remote_port, remote_host = (session.forward.active_remotes - old_active_remotes).first
395
+ @forwarded_ports[[local_port, local_host]] = [ remote_port, remote_host ]
396
+ end
397
+ [ remote_port, remote_host ]
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end