chef-metal 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +160 -0
- data/Rakefile +6 -0
- data/lib/chef/provider/fog_key_pair.rb +106 -0
- data/lib/chef/provider/machine.rb +60 -0
- data/lib/chef/provider/machine_file.rb +39 -0
- data/lib/chef/provider/vagrant_box.rb +44 -0
- data/lib/chef/provider/vagrant_cluster.rb +39 -0
- data/lib/chef/resource/fog_key_pair.rb +34 -0
- data/lib/chef/resource/machine.rb +56 -0
- data/lib/chef/resource/machine_file.rb +25 -0
- data/lib/chef/resource/vagrant_box.rb +18 -0
- data/lib/chef/resource/vagrant_cluster.rb +16 -0
- data/lib/chef_metal.rb +84 -0
- data/lib/chef_metal/aws_credentials.rb +55 -0
- data/lib/chef_metal/convergence_strategy.rb +15 -0
- data/lib/chef_metal/convergence_strategy/install_msi.rb +41 -0
- data/lib/chef_metal/convergence_strategy/install_sh.rb +36 -0
- data/lib/chef_metal/convergence_strategy/precreate_chef_objects.rb +140 -0
- data/lib/chef_metal/inline_resource.rb +88 -0
- data/lib/chef_metal/machine.rb +79 -0
- data/lib/chef_metal/machine/basic_machine.rb +79 -0
- data/lib/chef_metal/machine/unix_machine.rb +108 -0
- data/lib/chef_metal/machine/windows_machine.rb +94 -0
- data/lib/chef_metal/provisioner.rb +71 -0
- data/lib/chef_metal/provisioner/fog_provisioner.rb +378 -0
- data/lib/chef_metal/provisioner/vagrant_provisioner.rb +327 -0
- data/lib/chef_metal/recipe_dsl.rb +26 -0
- data/lib/chef_metal/transport.rb +36 -0
- data/lib/chef_metal/transport/ssh.rb +157 -0
- data/lib/chef_metal/transport/winrm.rb +91 -0
- data/lib/chef_metal/version.rb +3 -0
- metadata +175 -0
@@ -0,0 +1,327 @@
|
|
1
|
+
require 'chef/mixin/shell_out'
|
2
|
+
require 'chef_metal/provisioner'
|
3
|
+
|
4
|
+
module ChefMetal
|
5
|
+
class Provisioner
|
6
|
+
|
7
|
+
# Provisions machines in vagrant.
|
8
|
+
class VagrantProvisioner < Provisioner
|
9
|
+
|
10
|
+
include Chef::Mixin::ShellOut
|
11
|
+
|
12
|
+
# Create a new vagrant provisioner.
|
13
|
+
#
|
14
|
+
# ## Parameters
|
15
|
+
# cluster_path - path to the directory containing the vagrant files, which
|
16
|
+
# should have been created with the vagrant_cluster resource.
|
17
|
+
def initialize(cluster_path)
|
18
|
+
@cluster_path = cluster_path
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :cluster_path
|
22
|
+
|
23
|
+
# Acquire a machine, generally by provisioning it. Returns a Machine
|
24
|
+
# object pointing at the machine, allowing useful actions like setup,
|
25
|
+
# converge, execute, file and directory. The Machine object will have a
|
26
|
+
# "node" property which must be saved to the server (if it is any
|
27
|
+
# different from the original node object).
|
28
|
+
#
|
29
|
+
# ## Parameters
|
30
|
+
# provider - the provider object that is calling this method.
|
31
|
+
# node - node object (deserialized json) representing this machine. If
|
32
|
+
# the node has a provisioner_options hash in it, these will be used
|
33
|
+
# instead of options provided by the provisioner. TODO compare and
|
34
|
+
# fail if different?
|
35
|
+
# node will have node['normal']['provisioner_options'] in it with any options.
|
36
|
+
# It is a hash with this format:
|
37
|
+
#
|
38
|
+
# -- provisioner_url: vagrant:<cluster_path>
|
39
|
+
# -- vagrant_options: hash of properties of the "config"
|
40
|
+
# object, i.e. "vm.box" => "ubuntu12" and "vm.box_url"
|
41
|
+
# -- vagrant_config: string containing other vagrant config.
|
42
|
+
# Should assume the variable "config" represents machine config.
|
43
|
+
# Will be written verbatim into the vm's Vagrantfile.
|
44
|
+
# -- transport_options: hash of options specifying the transport.
|
45
|
+
# :type => :ssh
|
46
|
+
# :type => :winrm
|
47
|
+
# If not specified, ssh is used unless vm.guest is :windows. If that is
|
48
|
+
# the case, the windows options are used and the port forward for 5985
|
49
|
+
# is detected.
|
50
|
+
# -- up_timeout: maximum time, in seconds, to wait for vagrant
|
51
|
+
# to bring up the machine. Defaults to 10 minutes.
|
52
|
+
#
|
53
|
+
# node['normal']['provisioner_output'] will be populated with information
|
54
|
+
# about the created machine. For vagrant, it is a hash with this
|
55
|
+
# format:
|
56
|
+
#
|
57
|
+
# -- provisioner_url: vagrant_cluster://<current_node>/<cluster_path>
|
58
|
+
# -- vm_name: name of vagrant vm created
|
59
|
+
# -- vm_file_path: path to machine-specific vagrant config file
|
60
|
+
# on disk
|
61
|
+
# -- forwarded_ports: hash with key as guest_port => host_port
|
62
|
+
#
|
63
|
+
def acquire_machine(provider, node)
|
64
|
+
# Set up the modified node data
|
65
|
+
provisioner_options = node['normal']['provisioner_options']
|
66
|
+
vm_name = node['name']
|
67
|
+
old_provisioner_output = node['normal']['provisioner_output']
|
68
|
+
node['normal']['provisioner_output'] = provisioner_output = {
|
69
|
+
'provisioner_url' => provisioner_url(provider),
|
70
|
+
'vm_name' => vm_name,
|
71
|
+
'vm_file_path' => File.join(cluster_path, "#{vm_name}.vm")
|
72
|
+
}
|
73
|
+
# Preserve existing forwarded ports
|
74
|
+
provisioner_output['forwarded_ports'] = old_provisioner_output['forwarded_ports'] if old_provisioner_output
|
75
|
+
|
76
|
+
# TODO compare new options to existing and fail if we cannot change it
|
77
|
+
# over (perhaps introduce a boolean that will force a delete and recreate
|
78
|
+
# in such a case)
|
79
|
+
|
80
|
+
# Determine contents of vm file
|
81
|
+
vm_file_content = "Vagrant.configure('2') do |outer_config|\n"
|
82
|
+
vm_file_content << " outer_config.vm.define #{vm_name.inspect} do |config|\n"
|
83
|
+
merged_vagrant_options = { 'vm.hostname' => node['name'] }
|
84
|
+
merged_vagrant_options.merge!(provisioner_options['vagrant_options']) if provisioner_options['vagrant_options']
|
85
|
+
merged_vagrant_options.each_pair do |key, value|
|
86
|
+
vm_file_content << " config.#{key} = #{value.inspect}\n"
|
87
|
+
end
|
88
|
+
vm_file_content << provisioner_options['vagrant_config'] if provisioner_options['vagrant_config']
|
89
|
+
vm_file_content << " end\nend\n"
|
90
|
+
|
91
|
+
# Set up vagrant file
|
92
|
+
vm_file = ChefMetal.inline_resource(provider) do
|
93
|
+
file provisioner_output['vm_file_path'] do
|
94
|
+
content vm_file_content
|
95
|
+
action :create
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Check current status of vm
|
100
|
+
current_status = vagrant_status(vm_name)
|
101
|
+
up_timeout = provisioner_options['up_timeout'] || 10*60
|
102
|
+
|
103
|
+
if current_status != 'running'
|
104
|
+
# Run vagrant up if vm is not running
|
105
|
+
provider.converge_by "run vagrant up #{vm_name} (status was '#{current_status}')" do
|
106
|
+
result = shell_out("vagrant up #{vm_name}", :cwd => cluster_path, :timeout => up_timeout)
|
107
|
+
if result.exitstatus != 0
|
108
|
+
raise "vagrant up #{vm_name} failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
|
109
|
+
end
|
110
|
+
parse_vagrant_up(result.stdout, node)
|
111
|
+
end
|
112
|
+
elsif vm_file.updated_by_last_action?
|
113
|
+
# Run vagrant reload if vm is running and vm file changed
|
114
|
+
provider.converge_by "run vagrant reload #{vm_name}" do
|
115
|
+
result = shell_out("vagrant reload #{vm_name}", :cwd => cluster_path, :timeout => up_timeout)
|
116
|
+
if result.exitstatus != 0
|
117
|
+
raise "vagrant reload #{vm_name} failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
|
118
|
+
end
|
119
|
+
parse_vagrant_up(result.stdout, node)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Create machine object for callers to use
|
124
|
+
machine_for(node)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Connect to machine without acquiring it
|
128
|
+
def connect_to_machine(node)
|
129
|
+
machine_for(node)
|
130
|
+
end
|
131
|
+
|
132
|
+
def delete_machine(provider, node)
|
133
|
+
if node['normal'] && node['normal']['provisioner_output']
|
134
|
+
provisioner_output = node['normal']['provisioner_output']
|
135
|
+
else
|
136
|
+
provisioner_output = {}
|
137
|
+
end
|
138
|
+
vm_name = provisioner_output['vm_name'] || node['name']
|
139
|
+
current_status = vagrant_status(vm_name)
|
140
|
+
if current_status != 'not created'
|
141
|
+
provider.converge_by "run vagrant destroy -f #{vm_name} (status was '#{current_status}')" do
|
142
|
+
result = shell_out("vagrant destroy -f #{vm_name}", :cwd => cluster_path)
|
143
|
+
if result.exitstatus != 0
|
144
|
+
raise "vagrant destroy failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
convergence_strategy_for(node).delete_chef_objects(provider, node)
|
150
|
+
|
151
|
+
vm_file_path = provisioner_output['vm_file_path'] || File.join(cluster_path, "#{vm_name}.vm")
|
152
|
+
ChefMetal.inline_resource(provider) do
|
153
|
+
file vm_file_path do
|
154
|
+
action :delete
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def stop_machine(provider, node)
|
160
|
+
if node['normal'] && node['normal']['provisioner_output']
|
161
|
+
provisioner_output = node['normal']['provisioner_output']
|
162
|
+
else
|
163
|
+
provisioner_output = {}
|
164
|
+
end
|
165
|
+
vm_name = provisioner_output['vm_name'] || node['name']
|
166
|
+
current_status = vagrant_status(vm_name)
|
167
|
+
if current_status == 'running'
|
168
|
+
provider.converge_by "run vagrant halt #{vm_name} (status was '#{current_status}')" do
|
169
|
+
result = shell_out("vagrant halt #{vm_name}", :cwd => cluster_path)
|
170
|
+
if result.exitstatus != 0
|
171
|
+
raise "vagrant halt failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
# Used by vagrant_cluster and machine to get the string used to configure vagrant
|
179
|
+
def self.vagrant_config_string(vagrant_config, variable, line_prefix)
|
180
|
+
hostname = name.gsub(/[^A-Za-z0-9\-]/, '-')
|
181
|
+
|
182
|
+
result = ''
|
183
|
+
vagrant_config.each_pair do |key, value|
|
184
|
+
result += "#{line_prefix}#{variable}.#{key} = #{value.inspect}\n"
|
185
|
+
end
|
186
|
+
result
|
187
|
+
end
|
188
|
+
|
189
|
+
protected
|
190
|
+
|
191
|
+
def provisioner_url(provider)
|
192
|
+
"vagrant_cluster://#{provider.node['name']}#{cluster_path}"
|
193
|
+
end
|
194
|
+
|
195
|
+
def parse_vagrant_up(output, node)
|
196
|
+
# Grab forwarded port info
|
197
|
+
in_forwarding_ports = false
|
198
|
+
output.lines.each do |line|
|
199
|
+
if in_forwarding_ports
|
200
|
+
if line =~ /-- (\d+) => (\d+)/
|
201
|
+
node['normal']['provisioner_output']['forwarded_ports'][$1] = $2
|
202
|
+
else
|
203
|
+
in_forwarding_ports = false
|
204
|
+
end
|
205
|
+
elsif line =~ /Forwarding ports...$/
|
206
|
+
node['normal']['provisioner_output']['forwarded_ports'] = {}
|
207
|
+
in_forwarding_ports = true
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def machine_for(node)
|
213
|
+
if vagrant_option(node, 'vm.guest').to_s == 'windows'
|
214
|
+
require 'chef_metal/machine/windows_machine'
|
215
|
+
ChefMetal::Machine::WindowsMachine.new(node, transport_for(node), convergence_strategy_for(node))
|
216
|
+
else
|
217
|
+
require 'chef_metal/machine/unix_machine'
|
218
|
+
ChefMetal::Machine::UnixMachine.new(node, transport_for(node), convergence_strategy_for(node))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def convergence_strategy_for(node)
|
223
|
+
if vagrant_option(node, 'vm.guest').to_s == 'windows'
|
224
|
+
require 'chef_metal/convergence_strategy/install_msi'
|
225
|
+
ChefMetal::ConvergenceStrategy::InstallMsi.new
|
226
|
+
else
|
227
|
+
require 'chef_metal/convergence_strategy/install_sh'
|
228
|
+
ChefMetal::ConvergenceStrategy::InstallSh.new
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def transport_for(node)
|
233
|
+
if vagrant_option(node, 'vm.guest').to_s == 'windows'
|
234
|
+
create_winrm_transport(node)
|
235
|
+
else
|
236
|
+
create_ssh_transport(node)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def vagrant_option(node, option)
|
241
|
+
if node['normal']['provisioner_options'] &&
|
242
|
+
node['normal']['provisioner_options']['vagrant_options']
|
243
|
+
node['normal']['provisioner_options']['vagrant_options'][option]
|
244
|
+
else
|
245
|
+
nil
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def vagrant_status(name)
|
250
|
+
status_output = shell_out("vagrant status #{name}", :cwd => cluster_path).stdout
|
251
|
+
if status_output =~ /^#{name}\s+([^\n]+)\s+\(([^\n]+)\)$/m
|
252
|
+
$1
|
253
|
+
else
|
254
|
+
'not created'
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def create_winrm_transport(node)
|
259
|
+
require 'chef_metal/transport/winrm'
|
260
|
+
|
261
|
+
provisioner_output = node['default']['provisioner_output'] || {}
|
262
|
+
forwarded_ports = provisioner_output['forwarded_ports'] || {}
|
263
|
+
|
264
|
+
# TODO IPv6 loopback? What do we do for that?
|
265
|
+
hostname = vagrant_option(node, 'winrm.host') || '127.0.0.1'
|
266
|
+
port = vagrant_option(node, 'winrm.port') || forwarded_ports[5985] || 5985
|
267
|
+
endpoint = "http://#{hostname}:#{port}/wsman"
|
268
|
+
type = :plaintext
|
269
|
+
options = {
|
270
|
+
:user => vagrant_option(node, 'winrm.username') || 'vagrant',
|
271
|
+
:pass => vagrant_option(node, 'winrm.password') || 'vagrant',
|
272
|
+
:disable_sspi => true
|
273
|
+
}
|
274
|
+
|
275
|
+
ChefMetal::Transport::WinRM.new(endpoint, type, options)
|
276
|
+
end
|
277
|
+
|
278
|
+
def create_ssh_transport(node)
|
279
|
+
require 'chef_metal/transport/ssh'
|
280
|
+
|
281
|
+
vagrant_ssh_config = vagrant_ssh_config_for(node)
|
282
|
+
hostname = vagrant_ssh_config['HostName']
|
283
|
+
username = vagrant_ssh_config['User']
|
284
|
+
ssh_options = {
|
285
|
+
:port => vagrant_ssh_config['Port'],
|
286
|
+
:auth_methods => ['publickey'],
|
287
|
+
:user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
|
288
|
+
:paranoid => yes_or_no(vagrant_ssh_config['StrictHostKeyChecking']),
|
289
|
+
:keys => [ strip_quotes(vagrant_ssh_config['IdentityFile']) ],
|
290
|
+
:keys_only => yes_or_no(vagrant_ssh_config['IdentitiesOnly'])
|
291
|
+
}
|
292
|
+
ssh_options[:auth_methods] = %w(password) if yes_or_no(vagrant_ssh_config['PasswordAuthentication'])
|
293
|
+
options = {
|
294
|
+
:prefix => 'sudo '
|
295
|
+
}
|
296
|
+
ChefMetal::Transport::SSH.new(hostname, username, ssh_options, options)
|
297
|
+
end
|
298
|
+
|
299
|
+
def vagrant_ssh_config_for(node)
|
300
|
+
vagrant_ssh_config = {}
|
301
|
+
result = shell_out("vagrant ssh-config #{node['normal']['provisioner_output']['vm_name']}", :cwd => cluster_path)
|
302
|
+
result.stdout.lines.inject({}) do |result, line|
|
303
|
+
line =~ /^\s*(\S+)\s+(.+)/
|
304
|
+
vagrant_ssh_config[$1] = $2
|
305
|
+
end
|
306
|
+
vagrant_ssh_config
|
307
|
+
end
|
308
|
+
|
309
|
+
def yes_or_no(str)
|
310
|
+
case str
|
311
|
+
when 'yes'
|
312
|
+
true
|
313
|
+
else
|
314
|
+
false
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def strip_quotes(str)
|
319
|
+
if str[0] == '"' && str[-1] == '"' && str.size >= 2
|
320
|
+
str[1..-2]
|
321
|
+
else
|
322
|
+
str
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'chef_metal'
|
2
|
+
require 'chef_metal/provisioner/fog_provisioner'
|
3
|
+
|
4
|
+
class Chef
|
5
|
+
class Recipe
|
6
|
+
def with_provisioner(provisioner, &block)
|
7
|
+
ChefMetal.with_provisioner(provisioner, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def with_provisioner_options(provisioner_options, &block)
|
11
|
+
ChefMetal.with_provisioner_options(provisioner_options, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_vagrant_cluster(cluster_path, &block)
|
15
|
+
ChefMetal.with_vagrant_cluster(cluster_path, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_vagrant_box(box_name, vagrant_options = {}, &block)
|
19
|
+
ChefMetal.with_vagrant_box(box_name, vagrant_options, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_fog_ec2_provisioner(options = {}, &block)
|
23
|
+
ChefMetal.with_provisioner(ChefMetal::Provisioner::FogProvisioner.new({ :provider => 'AWS' }.merge(options)), &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ChefMetal
|
2
|
+
class Transport
|
3
|
+
def execute(command)
|
4
|
+
raise "execute not overridden on #{self.class}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def read_file(path)
|
8
|
+
raise "read_file not overridden on #{self.class}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def write_file(path, content)
|
12
|
+
raise "write_file not overridden on #{self.class}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def download_file(path, local_path)
|
16
|
+
IO.write(local_path, read_file(path))
|
17
|
+
end
|
18
|
+
|
19
|
+
def upload_file(local_path, path)
|
20
|
+
write_file(path, IO.read(local_path))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Forward requests to a port on the guest to a server on the host
|
24
|
+
def forward_remote_port_to_local(remote_port, local_port)
|
25
|
+
raise "forward_remote_port_to_local not overridden on #{self.class}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def disconnect
|
29
|
+
raise "disconnect not overridden on #{self.class}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def available?
|
33
|
+
raise "available? not overridden on #{self.class}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'chef_metal/transport'
|
2
|
+
|
3
|
+
module ChefMetal
|
4
|
+
class Transport
|
5
|
+
class SSH < Transport
|
6
|
+
def initialize(host, username, ssh_options, options)
|
7
|
+
require 'net/ssh'
|
8
|
+
require 'net/scp'
|
9
|
+
@host = host
|
10
|
+
@username = username
|
11
|
+
@ssh_options = ssh_options
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :host
|
16
|
+
attr_reader :username
|
17
|
+
attr_reader :ssh_options
|
18
|
+
attr_reader :options
|
19
|
+
|
20
|
+
def execute(command)
|
21
|
+
Chef::Log.info("Executing #{command} on #{username}@#{host}")
|
22
|
+
stdout = ''
|
23
|
+
stderr = ''
|
24
|
+
exitstatus = nil
|
25
|
+
channel = session.open_channel do |channel|
|
26
|
+
channel.exec("#{options[:prefix]}#{command}") do |ch, success|
|
27
|
+
raise "could not execute command: #{command.inspect}" unless success
|
28
|
+
|
29
|
+
channel.on_data do |ch2, data|
|
30
|
+
stdout << data
|
31
|
+
end
|
32
|
+
|
33
|
+
channel.on_extended_data do |ch2, type, data|
|
34
|
+
stderr << data
|
35
|
+
end
|
36
|
+
|
37
|
+
channel.on_request "exit-status" do |ch, data|
|
38
|
+
exitstatus = data.read_long
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
channel.wait
|
44
|
+
|
45
|
+
Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
|
46
|
+
Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != ''
|
47
|
+
Chef::Log.info("Stderr was:\n#{stderr}") if stderr != ''
|
48
|
+
SSHResult.new(stdout, stderr, exitstatus)
|
49
|
+
end
|
50
|
+
|
51
|
+
def read_file(path)
|
52
|
+
Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
|
53
|
+
result = StringIO.new
|
54
|
+
download(path, result)
|
55
|
+
result.string
|
56
|
+
end
|
57
|
+
|
58
|
+
def download_file(path, local_path)
|
59
|
+
Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
|
60
|
+
download(path, local_path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_file(path, content)
|
64
|
+
if options[:prefix]
|
65
|
+
# Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
|
66
|
+
remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
|
67
|
+
Chef::Log.debug("Writing #{content.length} bytes to #{remote_tempfile} on #{username}@#{host}")
|
68
|
+
Net::SCP.new(session).upload!(StringIO.new(content), remote_tempfile)
|
69
|
+
execute("mv #{remote_tempfile} #{path}")
|
70
|
+
else
|
71
|
+
Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
|
72
|
+
Net::SCP.new(session).upload!(StringIO.new(content), path)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def upload_file(local_path, path)
|
77
|
+
if options[:prefix]
|
78
|
+
# Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
|
79
|
+
remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
|
80
|
+
Chef::Log.debug("Uploading #{local_path} to #{remote_tempfile} on #{username}@#{host}")
|
81
|
+
Net::SCP.new(session).upload!(local_path, remote_tempfile)
|
82
|
+
execute("mv #{remote_tempfile} #{path}")
|
83
|
+
else
|
84
|
+
Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
|
85
|
+
Net::SCP.new(session).upload!(local_path, path)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def forward_remote_port_to_local(remote_port, local_port)
|
90
|
+
# TODO IPv6
|
91
|
+
Chef::Log.debug("Forwarding local server 127.0.0.1:#{local_port} to port #{remote_port} on #{username}@#{host}")
|
92
|
+
session.forward.remote(local_port, "127.0.0.1", remote_port)
|
93
|
+
end
|
94
|
+
|
95
|
+
def disconnect
|
96
|
+
if @session
|
97
|
+
begin
|
98
|
+
Chef::Log.debug("Closing SSH session on #{username}@#{host}")
|
99
|
+
@session.close
|
100
|
+
rescue
|
101
|
+
end
|
102
|
+
@session = nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def available?
|
107
|
+
execute('pwd')
|
108
|
+
true
|
109
|
+
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::AuthenticationFailed, Net::SSH::Disconnect, Net::SSH::HostKeyMismatch
|
110
|
+
Chef::Log.debug("#{username}@#{host} unavailable: could not execute 'pwd' on #{host}: #{$!.inspect}")
|
111
|
+
false
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def session
|
117
|
+
@session ||= begin
|
118
|
+
Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_options.inspect}")
|
119
|
+
Net::SSH.start(host, username, ssh_options)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def download(path, local_path)
|
124
|
+
channel = Net::SCP.new(session).download(path, local_path)
|
125
|
+
begin
|
126
|
+
channel.wait
|
127
|
+
rescue Net::SCP::Error
|
128
|
+
nil
|
129
|
+
rescue
|
130
|
+
# This works around https://github.com/net-ssh/net-scp/pull/10 until a new net-scp is merged.
|
131
|
+
begin
|
132
|
+
channel.close
|
133
|
+
channel.wait
|
134
|
+
rescue Net::SCP::Error
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class SSHResult
|
141
|
+
def initialize(stdout, stderr, exitstatus)
|
142
|
+
@stdout = stdout
|
143
|
+
@stderr = stderr
|
144
|
+
@exitstatus = exitstatus
|
145
|
+
end
|
146
|
+
|
147
|
+
attr_reader :stdout
|
148
|
+
attr_reader :stderr
|
149
|
+
attr_reader :exitstatus
|
150
|
+
|
151
|
+
def error!
|
152
|
+
raise "Error: code #{exitstatus}.\nSTDOUT:#{stdout}\nSTDERR:#{stderr}" if exitstatus != 0
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|