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,108 @@
|
|
1
|
+
require 'chef_metal/machine/basic_machine'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
module ChefMetal
|
5
|
+
class Machine
|
6
|
+
class UnixMachine < BasicMachine
|
7
|
+
def initialize(node, 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(provider, path)
|
18
|
+
if file_exists?(path)
|
19
|
+
provider.converge_by "delete file #{path} on #{node['name']}" do
|
20
|
+
transport.execute("rm -f #{path}").error!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return true or false depending on whether file exists
|
26
|
+
def file_exists?(path)
|
27
|
+
result = transport.execute("ls -d #{path}")
|
28
|
+
result.exitstatus == 0 && result.stdout != ''
|
29
|
+
end
|
30
|
+
|
31
|
+
def files_different?(path, local_path, content=nil)
|
32
|
+
if !file_exists?(path)
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get remote checksum of file
|
37
|
+
result = transport.execute("md5sum -b #{path}")
|
38
|
+
result.error!
|
39
|
+
remote_sum = result.stdout.split(' ')[0]
|
40
|
+
|
41
|
+
digest = Digest::MD5.new
|
42
|
+
if content
|
43
|
+
digest.update(content)
|
44
|
+
else
|
45
|
+
File.open(local_path, 'rb') do |io|
|
46
|
+
while (buf = io.read(4096)) && buf.length > 0
|
47
|
+
digest.update(buf)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
remote_sum != digest.hexdigest
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_dir(provider, path)
|
55
|
+
if !file_exists?(path)
|
56
|
+
provider.converge_by "create directory #{path} on #{node['name']}" do
|
57
|
+
transport.execute("mkdir #{path}").error!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set file attributes { mode, :owner, :group }
|
63
|
+
def set_attributes(provider, path, attributes)
|
64
|
+
if attributes[:mode] || attributes[:owner] || attributes[:group]
|
65
|
+
current_attributes = get_file_attributes(path)
|
66
|
+
if attributes[:mode] && current_attributes[:mode] != attributes[:mode]
|
67
|
+
provider.converge_by "change mode of #{path} on #{node['name']} from #{current_attributes[:mode].to_i(8)} to #{attributes[:mode].to_i(8)}" do
|
68
|
+
transport.execute("chmod #{attributes[:mode].to_i(8)} #{path}").error!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
if attributes[:owner] && current_attributes[:owner] != attributes[:owner]
|
72
|
+
provider.converge_by "change group of #{path} on #{node['name']} from #{current_attributes[:owner]} to #{attributes[:owner]}" do
|
73
|
+
transport.execute("chown #{attributes[:owner]} #{path}").error!
|
74
|
+
end
|
75
|
+
end
|
76
|
+
if attributes[:group] && current_attributes[:group] != attributes[:group]
|
77
|
+
provider.converge_by "change group of #{path} on #{node['name']} from #{current_attributes[:group]} to #{attributes[:group]}" do
|
78
|
+
transport.execute("chgrp #{attributes[:group]} #{path}").error!
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get file attributes { :mode, :owner, :group }
|
85
|
+
def get_attributes(path)
|
86
|
+
file_info = transport.execute("ls -ld #{path}").stdout.split(/\s+/)
|
87
|
+
if file_info.size <= 1
|
88
|
+
raise "#{path} does not exist in set_attributes()"
|
89
|
+
end
|
90
|
+
result = {
|
91
|
+
:mode => 0,
|
92
|
+
:owner => file_info[2],
|
93
|
+
:group => file_info[3]
|
94
|
+
}
|
95
|
+
attribute_string = file_info[0]
|
96
|
+
0.upto(attribute_string.length-1).each do |i|
|
97
|
+
result[:mode] <<= 1
|
98
|
+
result[:mode] += (attribute_string[i] == '-' ? 0 : 1)
|
99
|
+
end
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
def dirname_on_machine(path)
|
104
|
+
path.split('/')[0..-2].join('/')
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'chef_metal/machine/basic_machine'
|
2
|
+
|
3
|
+
module ChefMetal
|
4
|
+
class Machine
|
5
|
+
class WindowsMachine < BasicMachine
|
6
|
+
def initialize(node, transport, convergence_strategy)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
# Options include:
|
11
|
+
#
|
12
|
+
# command_prefix - prefix to put in front of any command, e.g. sudo
|
13
|
+
attr_reader :options
|
14
|
+
|
15
|
+
# Delete file
|
16
|
+
def delete_file(provider, path)
|
17
|
+
if file_exists?(path)
|
18
|
+
provider.converge_by "delete file #{escape(path)} on #{node['name']}" do
|
19
|
+
transport.execute("Remove-Item #{escape(path)}").error!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return true or false depending on whether file exists
|
25
|
+
def file_exists?(path)
|
26
|
+
parse_boolean(transport.execute("Test-Path #{escape(path)}").stdout)
|
27
|
+
end
|
28
|
+
|
29
|
+
def files_different?(path, local_path, content=nil)
|
30
|
+
# Get remote checksum of file (from http://stackoverflow.com/a/13926809)
|
31
|
+
result = transport.execute <<-EOM
|
32
|
+
$md5 = [System.Security.Cryptography.MD5]::Create("MD5")
|
33
|
+
$fd = [System.IO.File]::OpenRead(#{path.inspect})
|
34
|
+
$buf = new-object byte[] (1024*1024*8) # 8mb buffer
|
35
|
+
while (($read_len = $fd.Read($buf,0,$buf.length)) -eq $buf.length){
|
36
|
+
$total += $buf.length
|
37
|
+
$md5.TransformBlock($buf,$offset,$buf.length,$buf,$offset)
|
38
|
+
}
|
39
|
+
# finalize the last read
|
40
|
+
$md5.TransformFinalBlock($buf,0,$read_len)
|
41
|
+
$hash = $md5.Hash
|
42
|
+
# convert hash bytes to hex formatted string
|
43
|
+
$hash | foreach { $hash_txt += $_.ToString("x2") }
|
44
|
+
$hash_txt
|
45
|
+
EOM
|
46
|
+
result.error!
|
47
|
+
remote_sum = result.stdout.split(' ')[0]
|
48
|
+
digest = Digest::SHA256.new
|
49
|
+
if content
|
50
|
+
digest.update(content)
|
51
|
+
else
|
52
|
+
File.open(local_path, 'rb') do |io|
|
53
|
+
while (buf = io.read(4096)) && buf.length > 0
|
54
|
+
digest.update(buf)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
remote_sum != digest.hexdigest
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_dir(provider, path)
|
62
|
+
if !file_exists?(path)
|
63
|
+
provider.converge_by "create directory #{path} on #{node['name']}" do
|
64
|
+
transport.execute("New-Item #{escape(path)} -Type directory")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Set file attributes { :owner, :group, :rights }
|
70
|
+
# def set_attributes(provider, path, attributes)
|
71
|
+
# end
|
72
|
+
|
73
|
+
# Get file attributes { :owner, :group, :rights }
|
74
|
+
# def get_attributes(path)
|
75
|
+
# end
|
76
|
+
|
77
|
+
def dirname_on_machine(path)
|
78
|
+
path.split(/[\\\/]/)[0..-2].join('\\')
|
79
|
+
end
|
80
|
+
|
81
|
+
def escape(string)
|
82
|
+
transport.escape(string)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_boolean(string)
|
86
|
+
if string =~ /^\s*true\s*$/mi
|
87
|
+
true
|
88
|
+
else
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ChefMetal
|
2
|
+
class Provisioner
|
3
|
+
# Acquire a machine, generally by provisioning it. Returns a Machine
|
4
|
+
# object pointing at the machine, allowing useful actions like setup,
|
5
|
+
# converge, execute, file and directory. The Machine object will have a
|
6
|
+
# "node" property which must be saved to the server (if it is any
|
7
|
+
# different from the original node object).
|
8
|
+
#
|
9
|
+
# ## Parameters
|
10
|
+
# provider - the provider object that is calling this method.
|
11
|
+
# node - node object (deserialized json) representing this machine. If
|
12
|
+
# the node has a provisioner_options hash in it, these will be used
|
13
|
+
# instead of options provided by the provisioner. TODO compare and
|
14
|
+
# fail if different?
|
15
|
+
# node will have node['normal']['provisioner_options'] in it with any
|
16
|
+
# options. It is a hash with at least these options:
|
17
|
+
#
|
18
|
+
# -- provisioner_url: <provisioner url>
|
19
|
+
#
|
20
|
+
# node['normal']['provisioner_output'] will be populated with
|
21
|
+
# information about the created machine. For vagrant, it is a hash
|
22
|
+
# with at least these options:
|
23
|
+
#
|
24
|
+
# -- provisioner_url: <provisioner url>
|
25
|
+
#
|
26
|
+
def acquire_machine(provider, node)
|
27
|
+
raise "#{self.class} does not override acquire_machine"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Connect to a machine without acquiring it. This method will NOT make any
|
31
|
+
# changes to anything.
|
32
|
+
#
|
33
|
+
# ## Parameters
|
34
|
+
# node - node object (deserialized json) representing this machine. The
|
35
|
+
# node may have normal attributes "provisioner_options" and
|
36
|
+
# "provisioner_output" in it, representing the input and output of
|
37
|
+
# any prior "acquire_machine" process (if any).
|
38
|
+
#
|
39
|
+
def connect_to_machine(node)
|
40
|
+
raise "#{self.class} does not override connect_to_machine"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Delete the given machine (idempotent). Should destroy the machine,
|
44
|
+
# returning things to the state before acquire_machine was called.
|
45
|
+
def delete_machine(provider, node)
|
46
|
+
raise "#{self.class} does not override delete_machine"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Stop the given machine.
|
50
|
+
def stop_machine(provider, node)
|
51
|
+
raise "#{self.class} does not override stop_machine"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Provider notification that happens at the point a resource is declared
|
55
|
+
# (after all properties have been set on it)
|
56
|
+
def resource_created(machine)
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def save_node(provider, node, chef_server)
|
62
|
+
# Save the node and create the client. TODO strip automatic attributes first so we don't race with "current state"
|
63
|
+
ChefMetal.inline_resource(provider) do
|
64
|
+
chef_node node['name'] do
|
65
|
+
chef_server chef_server
|
66
|
+
raw_json node
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,378 @@
|
|
1
|
+
require 'chef_metal/provisioner'
|
2
|
+
require 'chef_metal/aws_credentials'
|
3
|
+
|
4
|
+
module ChefMetal
|
5
|
+
class Provisioner
|
6
|
+
|
7
|
+
# Provisions machines in vagrant.
|
8
|
+
class FogProvisioner < Provisioner
|
9
|
+
|
10
|
+
include Chef::Mixin::ShellOut
|
11
|
+
|
12
|
+
DEFAULT_OPTIONS = {
|
13
|
+
:create_timeout => 600,
|
14
|
+
:start_timeout => 600,
|
15
|
+
:ssh_timeout => 20
|
16
|
+
}
|
17
|
+
|
18
|
+
# Create a new vagrant provisioner.
|
19
|
+
#
|
20
|
+
# ## Parameters
|
21
|
+
# compute_options - hash of options to be passed to Fog::Compute.new
|
22
|
+
# Special options:
|
23
|
+
# - :base_bootstrap_options is merged with bootstrap_options in acquire_machine
|
24
|
+
# to present the full set of bootstrap options. Write down any bootstrap_options
|
25
|
+
# you intend to apply universally here.
|
26
|
+
# - :aws_credentials is an AWS CSV file (created with Download Credentials)
|
27
|
+
# containing your aws key information. If you do not specify aws_access_key_id
|
28
|
+
# and aws_secret_access_key explicitly, the first line from this file
|
29
|
+
# will be used. You may pass a Cheffish::AWSCredentials object.
|
30
|
+
# - :create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
|
31
|
+
# - :start_timeout - the time to wait for the instance to start (defaults to 600)
|
32
|
+
# - :ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
|
33
|
+
def initialize(compute_options)
|
34
|
+
@base_bootstrap_options = compute_options.delete(:base_bootstrap_options) || {}
|
35
|
+
if compute_options[:provider] == 'AWS'
|
36
|
+
aws_credentials = compute_options.delete(:aws_credentials)
|
37
|
+
if aws_credentials
|
38
|
+
@aws_credentials = aws_credentials
|
39
|
+
else
|
40
|
+
@aws_credentials = ChefMetal::AWSCredentials.new
|
41
|
+
@aws_credentials.load_default
|
42
|
+
end
|
43
|
+
compute_options[:aws_access_key_id] ||= @aws_credentials.default[:access_key_id]
|
44
|
+
compute_options[:aws_secret_access_key] ||= @aws_credentials.default[:secret_access_key]
|
45
|
+
end
|
46
|
+
@key_pairs = {}
|
47
|
+
@compute_options = compute_options
|
48
|
+
@base_bootstrap_options_for = {}
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :compute_options
|
52
|
+
attr_reader :aws_credentials
|
53
|
+
attr_reader :key_pairs
|
54
|
+
|
55
|
+
def current_base_bootstrap_options
|
56
|
+
result = @base_bootstrap_options.dup
|
57
|
+
if compute_options[:provider] == 'AWS'
|
58
|
+
if key_pairs.size > 0
|
59
|
+
last_pair_name = key_pairs.keys.last
|
60
|
+
last_pair = key_pairs[last_pair_name]
|
61
|
+
result[:key_name] ||= last_pair_name
|
62
|
+
result[:private_key_path] ||= last_pair.private_key_path
|
63
|
+
result[:public_key_path] ||= last_pair.public_key_path
|
64
|
+
end
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
# Acquire a machine, generally by provisioning it. Returns a Machine
|
70
|
+
# object pointing at the machine, allowing useful actions like setup,
|
71
|
+
# converge, execute, file and directory. The Machine object will have a
|
72
|
+
# "node" property which must be saved to the server (if it is any
|
73
|
+
# different from the original node object).
|
74
|
+
#
|
75
|
+
# ## Parameters
|
76
|
+
# provider - the provider object that is calling this method.
|
77
|
+
# node - node object (deserialized json) representing this machine. If
|
78
|
+
# the node has a provisioner_options hash in it, these will be used
|
79
|
+
# instead of options provided by the provisioner. TODO compare and
|
80
|
+
# fail if different?
|
81
|
+
# node will have node['normal']['provisioner_options'] in it with any options.
|
82
|
+
# It is a hash with this format:
|
83
|
+
#
|
84
|
+
# -- provisioner_url: fog:<relevant_fog_options>
|
85
|
+
# -- bootstrap_options: hash of options to pass to compute.servers.create
|
86
|
+
# -- is_windows: true if windows. TODO detect this from ami?
|
87
|
+
# -- create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
|
88
|
+
# -- start_timeout - the time to wait for the instance to start (defaults to 600)
|
89
|
+
# -- ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
|
90
|
+
#
|
91
|
+
# Example bootstrap_options for ec2:
|
92
|
+
# :image_id =>'ami-311f2b45',
|
93
|
+
# :flavor_id =>'t1.micro',
|
94
|
+
# :key_name => 'pey-pair-name'
|
95
|
+
#
|
96
|
+
# node['normal']['provisioner_output'] will be populated with information
|
97
|
+
# about the created machine. For vagrant, it is a hash with this
|
98
|
+
# format:
|
99
|
+
#
|
100
|
+
# -- provisioner_url: fog:<relevant_fog_options>
|
101
|
+
# -- server_id: the ID of the server so it can be found again
|
102
|
+
#
|
103
|
+
def acquire_machine(provider, node)
|
104
|
+
# Set up the modified node data
|
105
|
+
provisioner_options = node['normal']['provisioner_options'] || {}
|
106
|
+
provisioner_output = node['normal']['provisioner_output'] || {
|
107
|
+
'provisioner_url' => provisioner_url
|
108
|
+
}
|
109
|
+
|
110
|
+
if provisioner_output['provisioner_url'] != provisioner_url
|
111
|
+
raise "Switching providers for a machine is not currently supported! Use machine :destroy and then re-create the machine on the new provider."
|
112
|
+
end
|
113
|
+
|
114
|
+
node['normal']['provisioner_output'] = provisioner_output
|
115
|
+
|
116
|
+
if provisioner_output['server_id']
|
117
|
+
|
118
|
+
# If the server already exists, make sure it is up
|
119
|
+
|
120
|
+
# TODO verify that the server info matches the specification (ami, etc.)\
|
121
|
+
server = server_for(node)
|
122
|
+
if !server
|
123
|
+
Chef::Log.warn "Machine #{node['name']} (#{provisioner_output['server_id']} on #{provisioner_url}) is not associated with the ec2 account. Recreating ..."
|
124
|
+
need_to_create = true
|
125
|
+
elsif server.state == 'terminated' # Can't come back from that
|
126
|
+
Chef::Log.warn "Machine #{node['name']} (#{server.id} on #{provisioner_url}) is terminated. Recreating ..."
|
127
|
+
need_to_create = true
|
128
|
+
else
|
129
|
+
need_to_create = false
|
130
|
+
if !server.ready?
|
131
|
+
provider.converge_by "start machine #{node['name']} (#{server.id} on #{provisioner_url})" do
|
132
|
+
server.start
|
133
|
+
end
|
134
|
+
provider.converge_by "wait for machine #{node['name']} (#{server.id} on #{provisioner_url}) to be ready" do
|
135
|
+
wait_until_ready(server, option_for(node, :start_timeout))
|
136
|
+
end
|
137
|
+
else
|
138
|
+
wait_until_ready(server, option_for(node, :ssh_timeout))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
else
|
142
|
+
need_to_create = true
|
143
|
+
end
|
144
|
+
|
145
|
+
if need_to_create
|
146
|
+
# If the server does not exist, create it
|
147
|
+
bootstrap_options = bootstrap_options_for(provider.new_resource, node)
|
148
|
+
|
149
|
+
start_time = Time.now
|
150
|
+
timeout = option_for(node, :create_timeout)
|
151
|
+
|
152
|
+
description = [ "create machine #{node['name']} on #{provisioner_url}" ]
|
153
|
+
bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
|
154
|
+
server = nil
|
155
|
+
provider.converge_by description do
|
156
|
+
server = compute.servers.create(bootstrap_options)
|
157
|
+
provisioner_output['server_id'] = server.id
|
158
|
+
# Save quickly in case something goes wrong
|
159
|
+
save_node(provider, node, provider.new_resource.chef_server)
|
160
|
+
end
|
161
|
+
|
162
|
+
if server
|
163
|
+
# Re-retrieve the server in a more malleable form and wait for it to be ready
|
164
|
+
server = compute.servers.get(server.id)
|
165
|
+
provider.converge_by "machine #{node['name']} created as #{server.id} on #{provisioner_url}" do
|
166
|
+
end
|
167
|
+
# Wait for the machine to come up and for ssh to start listening
|
168
|
+
transport = nil
|
169
|
+
_self = self
|
170
|
+
provider.converge_by "wait for machine #{node['name']} to boot" do
|
171
|
+
server.wait_for(timeout - (Time.now - start_time)) do
|
172
|
+
if ready?
|
173
|
+
transport ||= _self.transport_for(server)
|
174
|
+
begin
|
175
|
+
transport.execute('pwd')
|
176
|
+
true
|
177
|
+
rescue Errno::ECONNREFUSED, Net::SSH::Disconnect
|
178
|
+
false
|
179
|
+
rescue
|
180
|
+
true
|
181
|
+
end
|
182
|
+
else
|
183
|
+
false
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# If there is some other error, we just wait patiently for SSH
|
189
|
+
begin
|
190
|
+
server.wait_for(option_for(node, :ssh_timeout)) { transport.available? }
|
191
|
+
rescue Fog::Errors::TimeoutError
|
192
|
+
# Sometimes (on EC2) the machine comes up but gets stuck or has
|
193
|
+
# some other problem. If this is the case, we restart the server
|
194
|
+
# to unstick it. Reboot covers a multitude of sins.
|
195
|
+
Chef::Log.warn "Machine #{node['name']} (#{server.id} on #{provisioner_url}) was started but SSH did not come up. Rebooting machine in an attempt to unstick it ..."
|
196
|
+
provider.converge_by "reboot machine #{node['name']} to try to unstick it" do
|
197
|
+
server.reboot
|
198
|
+
end
|
199
|
+
provider.converge_by "wait for machine #{node['name']} to be ready after reboot" do
|
200
|
+
wait_until_ready(server, option_for(node, :start_timeout))
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Create machine object for callers to use
|
207
|
+
machine_for(node, server)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Connect to machine without acquiring it
|
211
|
+
def connect_to_machine(node)
|
212
|
+
machine_for(node)
|
213
|
+
end
|
214
|
+
|
215
|
+
def delete_machine(provider, node)
|
216
|
+
if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
|
217
|
+
server = compute.servers.get(node['normal']['provisioner_output']['server_id'])
|
218
|
+
provider.converge_by "destroy machine #{node['name']} (#{server.id} at #{provisioner_url}" do
|
219
|
+
server.destroy
|
220
|
+
end
|
221
|
+
convergence_strategy_for(node).delete_chef_objects(provider, node)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def stop_machine(provider, node)
|
226
|
+
# If the machine doesn't exist, we silently do nothing
|
227
|
+
if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
|
228
|
+
server = compute.servers.get(node['normal']['provisioner_output']['server_id'])
|
229
|
+
provider.converge_by "stop machine #{node['name']} (#{server.id} at #{provisioner_url}" do
|
230
|
+
server.stop
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def resource_created(machine)
|
236
|
+
@base_bootstrap_options_for[machine] = current_base_bootstrap_options
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
def compute
|
241
|
+
@compute ||= begin
|
242
|
+
require 'fog/compute'
|
243
|
+
require 'fog'
|
244
|
+
Fog::Compute.new(compute_options)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def provisioner_url
|
249
|
+
provider_identifier = case compute_options[:provider]
|
250
|
+
when 'AWS'
|
251
|
+
compute_options[:aws_access_key_id]
|
252
|
+
else
|
253
|
+
'???'
|
254
|
+
end
|
255
|
+
"fog:#{compute_options['provider']}:#{provider_identifier}"
|
256
|
+
end
|
257
|
+
|
258
|
+
def transport_for(server)
|
259
|
+
# TODO winrm
|
260
|
+
create_ssh_transport(server)
|
261
|
+
end
|
262
|
+
|
263
|
+
protected
|
264
|
+
|
265
|
+
def option_for(node, key)
|
266
|
+
if node['normal']['provisioner_options'] && node['normal']['provisioner_options'][key.to_s]
|
267
|
+
node['normal']['provisioner_options'][key.to_s]
|
268
|
+
elsif compute_options[key]
|
269
|
+
compute_options[key]
|
270
|
+
else
|
271
|
+
DEFAULT_OPTIONS[key]
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def symbolize_keys(options)
|
276
|
+
options.inject({}) { |result,key,value| result[key.to_sym] = value; result }
|
277
|
+
end
|
278
|
+
|
279
|
+
def server_for(node)
|
280
|
+
if node['normal']['provisioner_output'] && node['normal']['provisioner_output']['server_id']
|
281
|
+
compute.servers.get(node['normal']['provisioner_output']['server_id'])
|
282
|
+
else
|
283
|
+
nil
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def bootstrap_options_for(machine, node)
|
288
|
+
provisioner_options = node['normal']['provisioner_options'] || {}
|
289
|
+
bootstrap_options = @base_bootstrap_options_for[machine] || current_base_bootstrap_options
|
290
|
+
bootstrap_options = bootstrap_options.merge(symbolize_keys(provisioner_options['bootstrap_options'] || {}))
|
291
|
+
require 'socket'
|
292
|
+
require 'etc'
|
293
|
+
tags = {
|
294
|
+
'Name' => node['name'],
|
295
|
+
'BootstrapChefServer' => machine.chef_server[:chef_server_url],
|
296
|
+
'BootstrapHost' => Socket.gethostname,
|
297
|
+
'BootstrapUser' => Etc.getlogin,
|
298
|
+
'BootstrapNodeName' => node['name']
|
299
|
+
}
|
300
|
+
if machine.chef_server[:options] && machine.chef_server[:options][:data_store]
|
301
|
+
tags['ChefLocalRepository'] = machine.chef_server[:options][:data_store].chef_fs.fs_description
|
302
|
+
end
|
303
|
+
# User-defined tags override the ones we set
|
304
|
+
tags.merge!(bootstrap_options[:tags]) if bootstrap_options[:tags]
|
305
|
+
bootstrap_options.merge!({ :tags => tags })
|
306
|
+
end
|
307
|
+
|
308
|
+
def machine_for(node, server = nil)
|
309
|
+
server ||= server_for(node)
|
310
|
+
if !server
|
311
|
+
raise "Server for node #{node['name']} has not been created!"
|
312
|
+
end
|
313
|
+
|
314
|
+
if node['normal']['provisioner_options'] && node['normal']['provisioner_options']['is_windows']
|
315
|
+
require 'chef_metal/machine/windows_machine'
|
316
|
+
ChefMetal::Machine::WindowsMachine.new(node, transport_for(server), convergence_strategy_for(node))
|
317
|
+
else
|
318
|
+
require 'chef_metal/machine/unix_machine'
|
319
|
+
ChefMetal::Machine::UnixMachine.new(node, transport_for(server), convergence_strategy_for(node))
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def convergence_strategy_for(node)
|
324
|
+
if node['normal']['provisioner_options'] && node['normal']['provisioner_options']['is_windows']
|
325
|
+
require 'chef_metal/convergence_strategy/install_msi'
|
326
|
+
ChefMetal::ConvergenceStrategy::InstallMsi.new
|
327
|
+
else
|
328
|
+
require 'chef_metal/convergence_strategy/install_sh'
|
329
|
+
ChefMetal::ConvergenceStrategy::InstallSh.new
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def ssh_options_for(server)
|
334
|
+
result = {
|
335
|
+
# :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
|
336
|
+
# :paranoid => yes_or_no(vagrant_ssh_config['StrictHostKeyChecking']),
|
337
|
+
:auth_methods => [ 'publickey' ],
|
338
|
+
:keys => [ server.private_key ],
|
339
|
+
:keys_only => true
|
340
|
+
}
|
341
|
+
if compute_options[:provider] == 'AWS'
|
342
|
+
# TODO generalize for others?
|
343
|
+
result[:keys] = [ server.private_key || key_pairs[server.key_name].private_key_path ]
|
344
|
+
result[:host_key_alias] = "#{server.id}.ec2"
|
345
|
+
else
|
346
|
+
private_key_path = nil
|
347
|
+
end
|
348
|
+
result
|
349
|
+
end
|
350
|
+
|
351
|
+
def create_ssh_transport(server)
|
352
|
+
require 'chef_metal/transport/ssh'
|
353
|
+
|
354
|
+
ssh_options = ssh_options_for(server)
|
355
|
+
options = {
|
356
|
+
:prefix => 'sudo '
|
357
|
+
}
|
358
|
+
ChefMetal::Transport::SSH.new(server.public_ip_address, 'ubuntu', ssh_options, options)
|
359
|
+
end
|
360
|
+
|
361
|
+
def wait_until_ready(server, timeout)
|
362
|
+
transport = nil
|
363
|
+
_self = self
|
364
|
+
server.wait_for(timeout) do
|
365
|
+
if transport
|
366
|
+
transport.available?
|
367
|
+
elsif ready?
|
368
|
+
# Don't create the transport until the machine is ready (we won't have the host till then)
|
369
|
+
transport = _self.transport_for(server)
|
370
|
+
transport.available?
|
371
|
+
else
|
372
|
+
false
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|