chef-provisioning 2.0.0 → 2.0.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +899 -885
  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 -35
  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 -156
  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 -29
  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 +5 -5
@@ -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