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,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
|