chef-metal 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 +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
|