chef-provisioning 0.15

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +207 -0
  3. data/LICENSE +201 -0
  4. data/README.md +260 -0
  5. data/Rakefile +6 -0
  6. data/lib/chef/provider/load_balancer.rb +77 -0
  7. data/lib/chef/provider/machine.rb +176 -0
  8. data/lib/chef/provider/machine_batch.rb +191 -0
  9. data/lib/chef/provider/machine_execute.rb +35 -0
  10. data/lib/chef/provider/machine_file.rb +54 -0
  11. data/lib/chef/provider/machine_image.rb +60 -0
  12. data/lib/chef/provisioning.rb +95 -0
  13. data/lib/chef/provisioning/action_handler.rb +68 -0
  14. data/lib/chef/provisioning/add_prefix_action_handler.rb +31 -0
  15. data/lib/chef/provisioning/chef_image_spec.rb +108 -0
  16. data/lib/chef/provisioning/chef_load_balancer_spec.rb +108 -0
  17. data/lib/chef/provisioning/chef_machine_spec.rb +84 -0
  18. data/lib/chef/provisioning/chef_provider_action_handler.rb +74 -0
  19. data/lib/chef/provisioning/chef_run_data.rb +139 -0
  20. data/lib/chef/provisioning/convergence_strategy.rb +28 -0
  21. data/lib/chef/provisioning/convergence_strategy/install_cached.rb +156 -0
  22. data/lib/chef/provisioning/convergence_strategy/install_msi.rb +58 -0
  23. data/lib/chef/provisioning/convergence_strategy/install_sh.rb +55 -0
  24. data/lib/chef/provisioning/convergence_strategy/no_converge.rb +39 -0
  25. data/lib/chef/provisioning/convergence_strategy/precreate_chef_objects.rb +183 -0
  26. data/lib/chef/provisioning/driver.rb +304 -0
  27. data/lib/chef/provisioning/image_spec.rb +72 -0
  28. data/lib/chef/provisioning/load_balancer_spec.rb +86 -0
  29. data/lib/chef/provisioning/machine.rb +112 -0
  30. data/lib/chef/provisioning/machine/basic_machine.rb +84 -0
  31. data/lib/chef/provisioning/machine/unix_machine.rb +278 -0
  32. data/lib/chef/provisioning/machine/windows_machine.rb +104 -0
  33. data/lib/chef/provisioning/machine_spec.rb +82 -0
  34. data/lib/chef/provisioning/recipe_dsl.rb +103 -0
  35. data/lib/chef/provisioning/transport.rb +95 -0
  36. data/lib/chef/provisioning/transport/ssh.rb +343 -0
  37. data/lib/chef/provisioning/transport/winrm.rb +151 -0
  38. data/lib/chef/provisioning/version.rb +5 -0
  39. data/lib/chef/resource/chef_data_bag_resource.rb +148 -0
  40. data/lib/chef/resource/load_balancer.rb +57 -0
  41. data/lib/chef/resource/machine.rb +124 -0
  42. data/lib/chef/resource/machine_batch.rb +78 -0
  43. data/lib/chef/resource/machine_execute.rb +28 -0
  44. data/lib/chef/resource/machine_file.rb +34 -0
  45. data/lib/chef/resource/machine_image.rb +35 -0
  46. data/lib/chef_metal.rb +1 -0
  47. metadata +217 -0
@@ -0,0 +1,104 @@
1
+ require 'chef/provisioning/machine/basic_machine'
2
+
3
+ class Chef
4
+ module Provisioning
5
+ class Machine
6
+ class WindowsMachine < BasicMachine
7
+ def initialize(machine_spec, transport, convergence_strategy)
8
+ super
9
+ end
10
+
11
+ # Options include:
12
+ #
13
+ # command_prefix - prefix to put in front of any command, e.g. sudo
14
+ attr_reader :options
15
+
16
+ # Delete file
17
+ def delete_file(action_handler, path)
18
+ if file_exists?(path)
19
+ action_handler.perform_action "delete file #{escape(path)} on #{machine_spec.name}" do
20
+ transport.execute("Remove-Item #{escape(path)}").error!
21
+ end
22
+ end
23
+ end
24
+
25
+ def is_directory?(path)
26
+ parse_boolean(transport.execute("Test-Path #{escape(path)} -pathtype container", :read_only => true).stdout)
27
+ end
28
+
29
+ # Return true or false depending on whether file exists
30
+ def file_exists?(path)
31
+ parse_boolean(transport.execute("Test-Path #{escape(path)}", :read_only => true).stdout)
32
+ end
33
+
34
+ def files_different?(path, local_path, content=nil)
35
+ if !file_exists?(path) || (local_path && !File.exists?(local_path))
36
+ return true
37
+ end
38
+
39
+ # Get remote checksum of file (from http://stackoverflow.com/a/13926809)
40
+ result = transport.execute(<<-EOM, :read_only => true)
41
+ $md5 = [System.Security.Cryptography.MD5]::Create("MD5")
42
+ $fd = [System.IO.File]::OpenRead(#{path.inspect})
43
+ $buf = new-object byte[] (1024*1024*8) # 8mb buffer
44
+ while (($read_len = $fd.Read($buf,0,$buf.length)) -eq $buf.length){
45
+ $total += $buf.length
46
+ $md5.TransformBlock($buf,$offset,$buf.length,$buf,$offset)
47
+ }
48
+ # finalize the last read
49
+ $md5.TransformFinalBlock($buf,0,$read_len)
50
+ $hash = $md5.Hash
51
+ # convert hash bytes to hex formatted string
52
+ $hash | foreach { $hash_txt += $_.ToString("x2") }
53
+ $hash_txt
54
+ EOM
55
+ result.error!
56
+ remote_sum = result.stdout.split(' ')[0]
57
+ digest = Digest::SHA256.new
58
+ if content
59
+ digest.update(content)
60
+ else
61
+ File.open(local_path, 'rb') do |io|
62
+ while (buf = io.read(4096)) && buf.length > 0
63
+ digest.update(buf)
64
+ end
65
+ end
66
+ end
67
+ remote_sum != digest.hexdigest
68
+ end
69
+
70
+ def create_dir(action_handler, path)
71
+ if !file_exists?(path)
72
+ action_handler.perform_action "create directory #{path} on #{machine_spec.name}" do
73
+ transport.execute("New-Item #{escape(path)} -Type directory")
74
+ end
75
+ end
76
+ end
77
+
78
+ # Set file attributes { :owner, :group, :rights }
79
+ # def set_attributes(action_handler, path, attributes)
80
+ # end
81
+
82
+ # Get file attributes { :owner, :group, :rights }
83
+ # def get_attributes(path)
84
+ # end
85
+
86
+ def dirname_on_machine(path)
87
+ path.split(/[\\\/]/)[0..-2].join('\\')
88
+ end
89
+
90
+ def escape(string)
91
+ transport.escape(string)
92
+ end
93
+
94
+ def parse_boolean(string)
95
+ if string =~ /^\s*true\s*$/mi
96
+ true
97
+ else
98
+ false
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,82 @@
1
+ class Chef
2
+ module Provisioning
3
+ #
4
+ # Specification for a machine. Sufficient information to find and contact it
5
+ # after it has been set up.
6
+ #
7
+ class MachineSpec
8
+ def initialize(node)
9
+ @node = node
10
+ # Upgrade from metal to chef_provisioning ASAP.
11
+ if node['normal'] && !node['normal']['chef_provisioning'] && node['normal']['metal']
12
+ node['normal']['chef_provisioning'] = node['normal'].delete('metal')
13
+ end
14
+ end
15
+
16
+ attr_reader :node
17
+
18
+ #
19
+ # Globally unique identifier for this machine. Does not depend on the machine's
20
+ # location or existence.
21
+ #
22
+ def id
23
+ raise "id unimplemented"
24
+ end
25
+
26
+ #
27
+ # Name of the machine. Corresponds to the name in "machine 'name' do" ...
28
+ #
29
+ def name
30
+ node['name']
31
+ end
32
+
33
+ #
34
+ # Location of this machine. This should be a freeform hash, with enough
35
+ # information for the driver to look it up and create a Machine object to
36
+ # access it.
37
+ #
38
+ # This MUST include a 'driver_url' attribute with the driver's URL in it.
39
+ #
40
+ # chef-provisioning will do its darnedest to not lose this information.
41
+ #
42
+ def location
43
+ chef_provisioning_attr('location')
44
+ end
45
+
46
+ #
47
+ # Set the location for this machine.
48
+ #
49
+ def location=(value)
50
+ set_chef_provisioning_attr('location', value)
51
+ end
52
+
53
+ # URL to the driver. Convenience for location['driver_url']
54
+ def driver_url
55
+ location ? location['driver_url'] : nil
56
+ end
57
+
58
+ #
59
+ # Save this node to the server. If you have significant information that
60
+ # could be lost, you should do this as quickly as possible. Data will be
61
+ # saved automatically for you after allocate_machine and ready_machine.
62
+ #
63
+ def save(action_handler)
64
+ raise "save unimplemented"
65
+ end
66
+
67
+ protected
68
+
69
+ def chef_provisioning_attr(attr)
70
+ if node['normal'] && node['normal']['chef_provisioning']
71
+ node['normal']['chef_provisioning'][attr]
72
+ end
73
+ end
74
+
75
+ def set_chef_provisioning_attr(attr, value)
76
+ node['normal'] ||= {}
77
+ node['normal']['chef_provisioning'] ||= {}
78
+ node['normal']['chef_provisioning'][attr] = value
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,103 @@
1
+ require 'chef/provisioning/chef_run_data'
2
+ require 'chef/resource_collection'
3
+ require 'chef/resource/chef_data_bag_resource'
4
+
5
+ require 'chef/resource/machine'
6
+ require 'chef/provider/machine'
7
+ require 'chef/resource/machine_batch'
8
+ require 'chef/provider/machine_batch'
9
+ require 'chef/resource/machine_file'
10
+ require 'chef/provider/machine_file'
11
+ require 'chef/resource/machine_execute'
12
+ require 'chef/provider/machine_execute'
13
+ require 'chef/resource/machine_image'
14
+ require 'chef/provider/machine_image'
15
+ require 'chef/resource/load_balancer'
16
+ require 'chef/provider/load_balancer'
17
+
18
+ class Chef
19
+ module DSL
20
+ module Recipe
21
+
22
+ def with_data_center(data_center, &block)
23
+ run_context.chef_metal.with_data_center(data_center, &block)
24
+ end
25
+
26
+ def with_driver(driver, options = nil, &block)
27
+ run_context.chef_provisioning.with_driver(driver, options, &block)
28
+ end
29
+
30
+ def with_machine_options(machine_options, &block)
31
+ run_context.chef_provisioning.with_machine_options(machine_options, &block)
32
+ end
33
+
34
+ def current_machine_options
35
+ run_context.chef_provisioning.current_machine_options
36
+ end
37
+
38
+ def add_machine_options(options, &block)
39
+ run_context.chef_provisioning.add_machine_options(options, &block)
40
+ end
41
+
42
+ def with_image_options(image_options, &block)
43
+ run_context.chef_provisioning.with_image_options(image_options, &block)
44
+ end
45
+
46
+ def current_image_options
47
+ run_context.chef_provisioning.current_image_options
48
+ end
49
+
50
+ NOT_PASSED = Object.new
51
+
52
+ @@next_machine_batch_index = 0
53
+
54
+ def machine_batch_default_name
55
+ @@next_machine_batch_index += 1
56
+ if @@next_machine_batch_index > 1
57
+ "default#{@@next_machine_batch_index}"
58
+ else
59
+ "default"
60
+ end
61
+ end
62
+
63
+ def machine_batch(name = nil, &block)
64
+ name ||= machine_batch_default_name
65
+ recipe = self
66
+ declare_resource(:machine_batch, name, caller[0]) do
67
+ from_recipe recipe
68
+ instance_eval(&block)
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+ class Config
76
+ default(:driver) { ENV['CHEF_DRIVER'] }
77
+ # config_context :drivers do
78
+ # # each key is a driver_url, and each value can have driver, driver_options and machine_options
79
+ # config_strict_mode false
80
+ # end
81
+ # config_context :driver_options do
82
+ # # open ended for whatever the driver wants
83
+ # config_strict_mode false
84
+ # end
85
+ # config_context :machine_options do
86
+ # # open ended for whatever the driver wants
87
+ # config_strict_mode false
88
+ # end
89
+ end
90
+
91
+ class RunContext
92
+ def chef_provisioning
93
+ @chef_provisioning ||= Chef::Provisioning::ChefRunData.new(config)
94
+ end
95
+ alias :chef_metal :chef_provisioning
96
+ end
97
+
98
+ class ResourceCollection
99
+ def previous_index
100
+ @insert_after_idx ? @insert_after_idx : @resources.length - 1
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,95 @@
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[:stream_stdout]
71
+ options[:stream_stdout].print stdout_chunk
72
+ elsif options[:stream] || config[:log_level] == :debug
73
+ STDOUT.print stdout_chunk
74
+ end
75
+ end
76
+ if stderr_chunk
77
+ if options[:stream_stderr]
78
+ options[:stream_stderr].print stderr_chunk
79
+ elsif options[:stream] || config[:log_level] == :debug
80
+ STDERR.print stderr_chunk
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def with_execute_timeout(options, &block)
87
+ Timeout::timeout(execute_timeout(options), &block)
88
+ end
89
+
90
+ def execute_timeout(options)
91
+ options.has_key?(:timeout) ? options[:timeout] : DEFAULT_TIMEOUT
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,343 @@
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.
28
+ # - global_config: an options hash that looks suspiciously similar to
29
+ # Chef::Config, containing at least the key :log_level.
30
+ #
31
+ # The options are used in
32
+ # Net::SSH.start(host, username, ssh_options)
33
+
34
+ def initialize(host, username, ssh_options, options, global_config)
35
+ @host = host
36
+ @username = username
37
+ @ssh_options = ssh_options
38
+ @options = options
39
+ @config = global_config
40
+ end
41
+
42
+ attr_reader :host
43
+ attr_reader :username
44
+ attr_reader :ssh_options
45
+ attr_reader :options
46
+ attr_reader :config
47
+
48
+ def execute(command, execute_options = {})
49
+ Chef::Log.info("Executing #{options[:prefix]}#{command} on #{username}@#{host}")
50
+ stdout = ''
51
+ stderr = ''
52
+ exitstatus = nil
53
+ session # grab session outside timeout, it has its own timeout
54
+ with_execute_timeout(execute_options) do
55
+ channel = session.open_channel do |channel|
56
+ # Enable PTY unless otherwise specified, some instances require this
57
+ unless options[:ssh_pty_enable] == false
58
+ channel.request_pty do |chan, success|
59
+ raise "could not get pty" if !success && options[:ssh_pty_enable]
60
+ end
61
+ end
62
+
63
+ channel.exec("#{options[:prefix]}#{command}") do |ch, success|
64
+ raise "could not execute command: #{command.inspect}" unless success
65
+
66
+ channel.on_data do |ch2, data|
67
+ stdout << data
68
+ stream_chunk(execute_options, data, nil)
69
+ end
70
+
71
+ channel.on_extended_data do |ch2, type, data|
72
+ stderr << data
73
+ stream_chunk(execute_options, nil, data)
74
+ end
75
+
76
+ channel.on_request "exit-status" do |ch, data|
77
+ exitstatus = data.read_long
78
+ end
79
+ end
80
+ end
81
+
82
+ channel.wait
83
+ end
84
+
85
+ Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
86
+ Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != '' && !options[:stream] && !options[:stream_stdout] && config[:log_level] != :debug
87
+ Chef::Log.info("Stderr was:\n#{stderr}") if stderr != '' && !options[:stream] && !options[:stream_stderr] && config[:log_level] != :debug
88
+ SSHResult.new(command, execute_options, stdout, stderr, exitstatus)
89
+ end
90
+
91
+ def read_file(path)
92
+ Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
93
+ result = StringIO.new
94
+ download(path, result)
95
+ result.string
96
+ end
97
+
98
+ def download_file(path, local_path)
99
+ Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
100
+ download(path, local_path)
101
+ end
102
+
103
+ def write_file(path, content)
104
+ execute("mkdir -p #{File.dirname(path)}").error!
105
+ if options[:prefix]
106
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
107
+ remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
108
+ Chef::Log.debug("Writing #{content.length} bytes to #{remote_tempfile} on #{username}@#{host}")
109
+ Net::SCP.new(session).upload!(StringIO.new(content), remote_tempfile)
110
+ execute("mv #{remote_tempfile} #{path}").error!
111
+ else
112
+ Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
113
+ Net::SCP.new(session).upload!(StringIO.new(content), path)
114
+ end
115
+ end
116
+
117
+ def upload_file(local_path, path)
118
+ execute("mkdir -p #{File.dirname(path)}").error!
119
+ if options[:prefix]
120
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
121
+ remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
122
+ Chef::Log.debug("Uploading #{local_path} to #{remote_tempfile} on #{username}@#{host}")
123
+ Net::SCP.new(session).upload!(local_path, remote_tempfile)
124
+ begin
125
+ execute("mv #{remote_tempfile} #{path}").error!
126
+ rescue
127
+ # Clean up if we were unable to move
128
+ execute("rm #{remote_tempfile}").error!
129
+ end
130
+ else
131
+ Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
132
+ Net::SCP.new(session).upload!(local_path, path)
133
+ end
134
+ end
135
+
136
+ def make_url_available_to_remote(local_url)
137
+ uri = URI(local_url)
138
+ if is_local_machine(uri.host)
139
+ port, host = forward_port(uri.port, uri.host, uri.port, 'localhost')
140
+ if !port
141
+ # Try harder if the port is already taken
142
+ port, host = forward_port(uri.port, uri.host, 0, 'localhost')
143
+ if !port
144
+ raise "Error forwarding port: could not forward #{uri.port} or 0"
145
+ end
146
+ end
147
+ uri.host = host
148
+ uri.port = port
149
+ end
150
+ 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.")
151
+ uri.to_s
152
+ end
153
+
154
+ def disconnect
155
+ if @session
156
+ begin
157
+ Chef::Log.debug("Closing SSH session on #{username}@#{host}")
158
+ @session.close
159
+ rescue
160
+ ensure
161
+ @session = nil
162
+ end
163
+ end
164
+ end
165
+
166
+ def available?
167
+ # If you can't pwd within 10 seconds, you can't pwd
168
+ execute('pwd', :timeout => 10)
169
+ true
170
+ rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EHOSTDOWN, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::Disconnect
171
+ Chef::Log.debug("#{username}@#{host} unavailable: network connection failed or broke: #{$!.inspect}")
172
+ disconnect
173
+ false
174
+ rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch
175
+ Chef::Log.debug("#{username}@#{host} unavailable: SSH authentication error: #{$!.inspect} ")
176
+ disconnect
177
+ false
178
+ end
179
+
180
+ protected
181
+
182
+ def session
183
+ @session ||= begin
184
+ ssh_start_opts = { timeout:10 }.merge(ssh_options)
185
+ Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_start_opts.inspect}")
186
+ # Small initial connection timeout (10s) to help us fail faster when server is just dead
187
+ begin
188
+ if gateway? then gateway.ssh(host, username, ssh_start_opts)
189
+ else Net::SSH.start(host, username, ssh_start_opts)
190
+ end
191
+ rescue Timeout::Error
192
+ Chef::Log.debug("Timed out connecting to SSH: #{$!}")
193
+ raise InitialConnectTimeout.new($!)
194
+ end
195
+ end
196
+ end
197
+
198
+ def download(path, local_path)
199
+ if options[:prefix]
200
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
201
+ remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
202
+ Chef::Log.debug("Downloading #{path} from #{remote_tempfile} to #{local_path} on #{username}@#{host}")
203
+ begin
204
+ execute("cp #{path} #{remote_tempfile}").error!
205
+ execute("chown #{username} #{remote_tempfile}").error!
206
+ do_download remote_tempfile, local_path
207
+ rescue => e
208
+ Chef::Log.error "Unable to download #{path} to #{local_path} on #{username}@#{host} -- #{e}"
209
+ nil
210
+ ensure
211
+ # Clean up afterwards
212
+ begin
213
+ execute("rm #{remote_tempfile}").error!
214
+ rescue => e
215
+ Chef::Log.warn "Unable to clean up #{remote_tempfile} on #{username}@#{host} -- #{e}"
216
+ end
217
+ end
218
+ else
219
+ do_download path, local_path
220
+ end
221
+ end
222
+
223
+ def do_download(path, local_path)
224
+ channel = Net::SCP.new(session).download(path, local_path)
225
+ begin
226
+ channel.wait
227
+ Chef::Log.debug "SCP completed for: #{path} to #{local_path}"
228
+ rescue Net::SCP::Error => e
229
+ Chef::Log.error "Error with SCP: #{e}"
230
+ # TODO we need a way to distinguish between "directory or file does not exist" and "SCP did not finish successfully"
231
+ nil
232
+ ensure
233
+ # ensure the channel is closed
234
+ channel.close
235
+ channel.wait
236
+ end
237
+
238
+ nil
239
+ end
240
+
241
+ class SSHResult
242
+ def initialize(command, options, stdout, stderr, exitstatus)
243
+ @command = command
244
+ @options = options
245
+ @stdout = stdout
246
+ @stderr = stderr
247
+ @exitstatus = exitstatus
248
+ end
249
+
250
+ attr_reader :command
251
+ attr_reader :options
252
+ attr_reader :stdout
253
+ attr_reader :stderr
254
+ attr_reader :exitstatus
255
+
256
+ def error!
257
+ if exitstatus != 0
258
+ # TODO stdout/stderr is already printed at info/debug level. Let's not print it twice, it's a lot.
259
+ msg = "Error: command '#{command}' exited with code #{exitstatus}.\n"
260
+ raise msg
261
+ end
262
+ end
263
+ end
264
+
265
+ class InitialConnectTimeout < Timeout::Error
266
+ def initialize(original_error)
267
+ super(original_error.message)
268
+ @original_error = original_error
269
+ end
270
+
271
+ attr_reader :original_error
272
+ end
273
+
274
+ private
275
+
276
+ def gateway?
277
+ options.key?(:ssh_gateway) and ! options[:ssh_gateway].nil?
278
+ end
279
+
280
+ def gateway
281
+ gw_user, gw_host = options[:ssh_gateway].split('@')
282
+ gw_host, gw_port = gw_host.split(':')
283
+ gw_user = ssh_options[:ssh_username] unless gw_user
284
+
285
+ ssh_start_opts = { timeout:10 }.merge(ssh_options)
286
+ ssh_start_opts[:port] = gw_port || 22
287
+
288
+ Chef::Log.debug("Opening SSH gateway to #{gw_user}@#{gw_host} with options #{ssh_start_opts.inspect}")
289
+ begin
290
+ Net::SSH::Gateway.new(gw_host, gw_user, ssh_start_opts)
291
+ rescue Errno::ETIMEDOUT
292
+ Chef::Log.debug("Timed out connecting to gateway: #{$!}")
293
+ raise InitialConnectTimeout.new($!)
294
+ end
295
+ end
296
+
297
+ def is_local_machine(host)
298
+ local_addrs = Socket.ip_address_list
299
+ host_addrs = Addrinfo.getaddrinfo(host, nil)
300
+ local_addrs.any? do |local_addr|
301
+ host_addrs.any? do |host_addr|
302
+ local_addr.ip_address == host_addr.ip_address
303
+ end
304
+ end
305
+ end
306
+
307
+ # Forwards a port over the connection, and returns the
308
+ def forward_port(local_port, local_host, remote_port, remote_host)
309
+ # This bit is from the documentation.
310
+ if session.forward.respond_to?(:active_remote_destinations)
311
+ got_remote_port, remote_host = session.forward.active_remote_destinations[[local_port, local_host]]
312
+ if !got_remote_port
313
+ Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
314
+
315
+ session.forward.remote(local_port, local_host, remote_port, remote_host) do |actual_remote_port|
316
+ got_remote_port = actual_remote_port || :error
317
+ :no_exception # I'll take care of it myself, thanks
318
+ end
319
+ # Kick SSH until we get a response
320
+ session.loop { !got_remote_port }
321
+ if got_remote_port == :error
322
+ return nil
323
+ end
324
+ end
325
+ [ got_remote_port, remote_host ]
326
+ else
327
+ @forwarded_ports ||= {}
328
+ remote_port, remote_host = @forwarded_ports[[local_port, local_host]]
329
+ if !remote_port
330
+ Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
331
+ old_active_remotes = session.forward.active_remotes
332
+ session.forward.remote(local_port, local_host, local_port)
333
+ session.loop { !(session.forward.active_remotes.length > old_active_remotes.length) }
334
+ remote_port, remote_host = (session.forward.active_remotes - old_active_remotes).first
335
+ @forwarded_ports[[local_port, local_host]] = [ remote_port, remote_host ]
336
+ end
337
+ [ remote_port, remote_host ]
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end