chef-provisioning 0.15

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