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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +899 -885
- data/Gemfile +17 -17
- data/LICENSE +201 -201
- data/README.md +312 -312
- data/Rakefile +55 -55
- data/chef-provisioning.gemspec +38 -38
- data/lib/chef/provider/load_balancer.rb +75 -75
- data/lib/chef/provider/machine.rb +219 -219
- data/lib/chef/provider/machine_batch.rb +224 -224
- data/lib/chef/provider/machine_execute.rb +36 -35
- data/lib/chef/provider/machine_file.rb +55 -55
- data/lib/chef/provider/machine_image.rb +105 -105
- data/lib/chef/provisioning.rb +110 -110
- data/lib/chef/provisioning/action_handler.rb +68 -68
- data/lib/chef/provisioning/add_prefix_action_handler.rb +35 -35
- data/lib/chef/provisioning/chef_managed_entry_store.rb +128 -128
- data/lib/chef/provisioning/chef_provider_action_handler.rb +74 -74
- data/lib/chef/provisioning/chef_run_data.rb +132 -132
- data/lib/chef/provisioning/convergence_strategy.rb +28 -28
- data/lib/chef/provisioning/convergence_strategy/ignore_convergence_failure.rb +54 -54
- data/lib/chef/provisioning/convergence_strategy/install_cached.rb +188 -188
- data/lib/chef/provisioning/convergence_strategy/install_msi.rb +71 -71
- data/lib/chef/provisioning/convergence_strategy/install_sh.rb +71 -71
- data/lib/chef/provisioning/convergence_strategy/no_converge.rb +35 -35
- data/lib/chef/provisioning/convergence_strategy/precreate_chef_objects.rb +255 -255
- data/lib/chef/provisioning/driver.rb +323 -323
- data/lib/chef/provisioning/load_balancer_spec.rb +14 -14
- data/lib/chef/provisioning/machine.rb +112 -112
- data/lib/chef/provisioning/machine/basic_machine.rb +84 -84
- data/lib/chef/provisioning/machine/unix_machine.rb +288 -288
- data/lib/chef/provisioning/machine/windows_machine.rb +108 -108
- data/lib/chef/provisioning/machine_image_spec.rb +34 -34
- data/lib/chef/provisioning/machine_spec.rb +58 -58
- data/lib/chef/provisioning/managed_entry.rb +121 -121
- data/lib/chef/provisioning/managed_entry_store.rb +136 -136
- data/lib/chef/provisioning/recipe_dsl.rb +99 -99
- data/lib/chef/provisioning/rspec.rb +27 -27
- data/lib/chef/provisioning/transport.rb +100 -100
- data/lib/chef/provisioning/transport/ssh.rb +403 -403
- data/lib/chef/provisioning/transport/winrm.rb +144 -156
- data/lib/chef/provisioning/version.rb +5 -5
- data/lib/chef/resource/chef_data_bag_resource.rb +146 -146
- data/lib/chef/resource/load_balancer.rb +57 -57
- data/lib/chef/resource/machine.rb +128 -128
- data/lib/chef/resource/machine_batch.rb +78 -78
- data/lib/chef/resource/machine_execute.rb +30 -29
- data/lib/chef/resource/machine_file.rb +34 -34
- data/lib/chef/resource/machine_image.rb +35 -35
- data/lib/chef_metal.rb +1 -1
- data/spec/chef/provisioning/convergence_strategy/ignore_convergence_failure_spec.rb +86 -86
- data/spec/spec_helper.rb +27 -27
- 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
|