chef-provisioning 0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +207 -0
- data/LICENSE +201 -0
- data/README.md +260 -0
- data/Rakefile +6 -0
- data/lib/chef/provider/load_balancer.rb +77 -0
- data/lib/chef/provider/machine.rb +176 -0
- data/lib/chef/provider/machine_batch.rb +191 -0
- data/lib/chef/provider/machine_execute.rb +35 -0
- data/lib/chef/provider/machine_file.rb +54 -0
- data/lib/chef/provider/machine_image.rb +60 -0
- data/lib/chef/provisioning.rb +95 -0
- data/lib/chef/provisioning/action_handler.rb +68 -0
- data/lib/chef/provisioning/add_prefix_action_handler.rb +31 -0
- data/lib/chef/provisioning/chef_image_spec.rb +108 -0
- data/lib/chef/provisioning/chef_load_balancer_spec.rb +108 -0
- data/lib/chef/provisioning/chef_machine_spec.rb +84 -0
- data/lib/chef/provisioning/chef_provider_action_handler.rb +74 -0
- data/lib/chef/provisioning/chef_run_data.rb +139 -0
- data/lib/chef/provisioning/convergence_strategy.rb +28 -0
- data/lib/chef/provisioning/convergence_strategy/install_cached.rb +156 -0
- data/lib/chef/provisioning/convergence_strategy/install_msi.rb +58 -0
- data/lib/chef/provisioning/convergence_strategy/install_sh.rb +55 -0
- data/lib/chef/provisioning/convergence_strategy/no_converge.rb +39 -0
- data/lib/chef/provisioning/convergence_strategy/precreate_chef_objects.rb +183 -0
- data/lib/chef/provisioning/driver.rb +304 -0
- data/lib/chef/provisioning/image_spec.rb +72 -0
- data/lib/chef/provisioning/load_balancer_spec.rb +86 -0
- data/lib/chef/provisioning/machine.rb +112 -0
- data/lib/chef/provisioning/machine/basic_machine.rb +84 -0
- data/lib/chef/provisioning/machine/unix_machine.rb +278 -0
- data/lib/chef/provisioning/machine/windows_machine.rb +104 -0
- data/lib/chef/provisioning/machine_spec.rb +82 -0
- data/lib/chef/provisioning/recipe_dsl.rb +103 -0
- data/lib/chef/provisioning/transport.rb +95 -0
- data/lib/chef/provisioning/transport/ssh.rb +343 -0
- data/lib/chef/provisioning/transport/winrm.rb +151 -0
- data/lib/chef/provisioning/version.rb +5 -0
- data/lib/chef/resource/chef_data_bag_resource.rb +148 -0
- data/lib/chef/resource/load_balancer.rb +57 -0
- data/lib/chef/resource/machine.rb +124 -0
- data/lib/chef/resource/machine_batch.rb +78 -0
- data/lib/chef/resource/machine_execute.rb +28 -0
- data/lib/chef/resource/machine_file.rb +34 -0
- data/lib/chef/resource/machine_image.rb +35 -0
- data/lib/chef_metal.rb +1 -0
- 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
|