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,15 @@
|
|
1
|
+
module ChefMetal
|
2
|
+
class ConvergenceStrategy
|
3
|
+
def setup_convergence(provider, machine, machine_resource)
|
4
|
+
raise "setup_convergence not overridden on #{self.class}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def converge(provider, machine)
|
8
|
+
raise "converge not overridden on #{self.class}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def delete_chef_objects(provider, node)
|
12
|
+
raise "delete_chef_objects not overridden on #{self.class}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'chef_metal/convergence_strategy/precreate_chef_objects'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module ChefMetal
|
5
|
+
class ConvergenceStrategy
|
6
|
+
class InstallMsi < PrecreateChefObjects
|
7
|
+
@@install_msi_cache = {}
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@install_msi_url = options[:install_msi_url] || 'http://www.opscode.com/chef/install.msi'
|
11
|
+
@install_msi_path = options[:install_msi_path] || "%TEMP%\\#{File.basename(@install_msi_url)}"
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :install_msi_url
|
15
|
+
attr_reader :install_msi_path
|
16
|
+
|
17
|
+
def setup_convergence(provider, machine, machine_resource)
|
18
|
+
system_drive = machine.execute_always('$env:SystemDrive').stdout.strip
|
19
|
+
@client_rb_path ||= "#{system_drive}\\chef\\client.rb"
|
20
|
+
@client_pem_path ||= "#{system_drive}\\chef\\client.pem"
|
21
|
+
|
22
|
+
super
|
23
|
+
|
24
|
+
# Install chef-client. TODO check and update version if not latest / not desired
|
25
|
+
if machine.execute_always('chef-client -v').exitstatus != 0
|
26
|
+
# TODO ssh verification of install.sh before running arbtrary code would be nice?
|
27
|
+
# TODO find a way to cache this on the host like with the Unix stuff.
|
28
|
+
# Limiter is we don't know how to efficiently upload large files to
|
29
|
+
# the remote machine with WMI.
|
30
|
+
machine.execute(provider, "(New-Object System.Net.WebClient).DownloadFile(#{machine.escape(install_msi_url)}, #{machine.escape(install_msi_path)})")
|
31
|
+
machine.execute(provider, "msiexec /qn /i #{machine.escape(install_msi_path)}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def converge(provider, machine)
|
36
|
+
# TODO For some reason I get a 500 back if I don't do -l debug
|
37
|
+
machine.transport.execute("chef-client -l debug")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'chef_metal/convergence_strategy/precreate_chef_objects'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module ChefMetal
|
5
|
+
class ConvergenceStrategy
|
6
|
+
class InstallSh < PrecreateChefObjects
|
7
|
+
@@install_sh_cache = {}
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@install_sh_url = options[:install_sh_url] || 'http://www.opscode.com/chef/install.sh'
|
11
|
+
@install_sh_path = options[:install_sh_path] || '/tmp/chef-install.sh'
|
12
|
+
@client_rb_path ||= '/etc/chef/client.rb'
|
13
|
+
@client_pem_path ||= '/etc/chef/client.pem'
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :install_sh_url
|
17
|
+
attr_reader :install_sh_path
|
18
|
+
|
19
|
+
def setup_convergence(provider, machine, machine_resource)
|
20
|
+
super
|
21
|
+
|
22
|
+
# Install chef-client. TODO check and update version if not latest / not desired
|
23
|
+
if machine.execute_always('chef-client -v').exitstatus != 0
|
24
|
+
# TODO ssh verification of install.sh before running arbtrary code would be nice?
|
25
|
+
@@install_sh_cache[install_sh_url] ||= Net::HTTP.get(URI(install_sh_url))
|
26
|
+
machine.write_file(provider, install_sh_path, @@install_sh_cache[install_sh_url], :ensure_dir => true)
|
27
|
+
machine.execute(provider, "bash #{install_sh_path}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def converge(provider, machine)
|
32
|
+
machine.execute(provider, 'chef-client')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'chef_metal/convergence_strategy'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module ChefMetal
|
5
|
+
class ConvergenceStrategy
|
6
|
+
class PrecreateChefObjects < ConvergenceStrategy
|
7
|
+
def initialize(options = {})
|
8
|
+
super
|
9
|
+
@client_rb_path = options[:client_rb_path]
|
10
|
+
@client_pem_path = options[:client_pem_path]
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :client_rb_path
|
14
|
+
attr_reader :client_pem_path
|
15
|
+
|
16
|
+
def setup_convergence(provider, machine, machine_resource)
|
17
|
+
# Create keys on machine
|
18
|
+
public_key = create_keys(provider, machine, machine_resource)
|
19
|
+
# Create node and client on chef server
|
20
|
+
create_chef_objects(provider, machine, machine_resource, public_key)
|
21
|
+
|
22
|
+
# If the chef server lives on localhost, tunnel the port through to the guest
|
23
|
+
chef_server_url = machine_resource.chef_server[:chef_server_url]
|
24
|
+
url = URI(chef_server_url)
|
25
|
+
# TODO IPv6
|
26
|
+
if is_localhost(url.host)
|
27
|
+
machine.forward_remote_port_to_local(url.port, url.port)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create client.rb and client.pem on machine
|
31
|
+
content = client_rb_content(chef_server_url, machine.node['name'])
|
32
|
+
machine.write_file(provider, client_rb_path, content, :ensure_dir => true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_chef_objects(provider, node)
|
36
|
+
ChefMetal.inline_resource(provider) do
|
37
|
+
chef_node node['name'] do
|
38
|
+
action :delete
|
39
|
+
end
|
40
|
+
chef_client node['name'] do
|
41
|
+
action :delete
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def create_keys(provider, machine, machine_resource)
|
49
|
+
server_private_key = machine.read_file(client_pem_path)
|
50
|
+
if server_private_key
|
51
|
+
begin
|
52
|
+
server_private_key, format = Cheffish::KeyFormatter.decode(server_private_key)
|
53
|
+
rescue
|
54
|
+
server_private_key = nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if server_private_key
|
59
|
+
source_key = source_key_for(machine_resource)
|
60
|
+
if source_key && server_private_key.to_pem != source_key.to_pem
|
61
|
+
# If the server private key does not match our source key, overwrite it
|
62
|
+
server_private_key = source_key
|
63
|
+
if machine_resource.allow_overwrite_keys
|
64
|
+
machine.write_file(provider, client_pem_path, server_private_key.to_pem, :ensure_dir => true)
|
65
|
+
else
|
66
|
+
raise "Private key on machine #{machine_resource.name} does not match desired input key."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
else
|
71
|
+
|
72
|
+
# If the server does not already have keys, create them and upload
|
73
|
+
Cheffish.inline_resource(provider) do
|
74
|
+
private_key 'in_memory' do
|
75
|
+
path :none
|
76
|
+
if machine_resource.private_key_options
|
77
|
+
machine_resource.private_key_options.each_pair do |key,value|
|
78
|
+
send(key, value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
after { |resource, private_key| server_private_key = private_key }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
machine.write_file(provider, client_pem_path, server_private_key.to_pem, :ensure_dir => true)
|
86
|
+
end
|
87
|
+
|
88
|
+
server_private_key.public_key
|
89
|
+
end
|
90
|
+
|
91
|
+
def is_localhost(host)
|
92
|
+
host == '127.0.0.1' || host == 'localhost' || host == '[::1]'
|
93
|
+
end
|
94
|
+
|
95
|
+
def source_key_for(machine_resource)
|
96
|
+
if machine_resource.source_key.is_a?(String)
|
97
|
+
key, format = Cheffish::KeyFormatter.decode(machine_resource.source_key, machine_resource.source_key_pass_phrase)
|
98
|
+
key
|
99
|
+
elsif machine_resource.source_key
|
100
|
+
machine_resource.source_key
|
101
|
+
elsif machine_resource.source_key_path
|
102
|
+
key, format = Cheffish::KeyFormatter.decode(IO.read(machine_resource.source_key_path), machine_resource.source_key_pass_phrase, machine_resource.source_key_path)
|
103
|
+
key
|
104
|
+
else
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_chef_objects(provider, machine, machine_resource, public_key)
|
110
|
+
# Save the node and create the client keys and client.
|
111
|
+
ChefMetal.inline_resource(provider) do
|
112
|
+
# Create node
|
113
|
+
# TODO strip automatic attributes first so we don't race with "current state"
|
114
|
+
chef_node machine.node['name'] do
|
115
|
+
chef_server machine_resource.chef_server
|
116
|
+
raw_json machine.node
|
117
|
+
end
|
118
|
+
|
119
|
+
# Create client
|
120
|
+
chef_client machine.node['name'] do
|
121
|
+
chef_server machine_resource.chef_server
|
122
|
+
source_key public_key
|
123
|
+
output_key_path machine_resource.public_key_path
|
124
|
+
output_key_format machine_resource.public_key_format
|
125
|
+
admin machine_resource.admin
|
126
|
+
validator machine_resource.validator
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def client_rb_content(chef_server_url, node_name)
|
132
|
+
<<EOM
|
133
|
+
chef_server_url #{chef_server_url.inspect}
|
134
|
+
node_name #{node_name.inspect}
|
135
|
+
client_key #{client_pem_path.inspect}
|
136
|
+
EOM
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module ChefMetal
|
2
|
+
class InlineResource
|
3
|
+
def initialize(provider)
|
4
|
+
@provider = provider
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :provider
|
8
|
+
|
9
|
+
def run_context
|
10
|
+
provider.run_context
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(method_symbol, *args, &block)
|
14
|
+
# Stolen ruthlessly from Chef's chef/dsl/recipe.rb
|
15
|
+
|
16
|
+
# Checks the new platform => short_name => resource mapping initially
|
17
|
+
# then fall back to the older approach (Chef::Resource.const_get) for
|
18
|
+
# backward compatibility
|
19
|
+
resource_class = Chef::Resource.resource_for_node(method_symbol, provider.run_context.node)
|
20
|
+
|
21
|
+
super unless resource_class
|
22
|
+
raise ArgumentError, "You must supply a name when declaring a #{method_symbol} resource" unless args.size > 0
|
23
|
+
|
24
|
+
# If we have a resource like this one, we want to steal its state
|
25
|
+
args << run_context
|
26
|
+
resource = resource_class.new(*args)
|
27
|
+
resource.source_line = caller[0]
|
28
|
+
resource.load_prior_resource
|
29
|
+
resource.cookbook_name = provider.cookbook_name
|
30
|
+
resource.recipe_name = @recipe_name
|
31
|
+
resource.params = @params
|
32
|
+
# Determine whether this resource is being created in the context of an enclosing Provider
|
33
|
+
resource.enclosing_provider = provider.is_a?(Chef::Provider) ? provider : nil
|
34
|
+
# Evaluate resource attribute DSL
|
35
|
+
resource.instance_eval(&block) if block
|
36
|
+
|
37
|
+
# Run optional resource hook
|
38
|
+
resource.after_created
|
39
|
+
|
40
|
+
# Do NOT put this in the resource collection.
|
41
|
+
#run_context.resource_collection.insert(resource)
|
42
|
+
|
43
|
+
# Instead, run the action directly.
|
44
|
+
Array(resource.action).each do |action|
|
45
|
+
resource.updated_by_last_action(false)
|
46
|
+
run_provider_action(resource.provider_for_action(action))
|
47
|
+
provider.new_resource.updated_by_last_action(true) if resource.updated_by_last_action?
|
48
|
+
end
|
49
|
+
resource
|
50
|
+
end
|
51
|
+
|
52
|
+
# Do Chef::Provider.run_action, but without events
|
53
|
+
def run_provider_action(inline_provider)
|
54
|
+
if !inline_provider.whyrun_supported?
|
55
|
+
raise "#{inline_provider} is not why-run-safe. Only why-run-safe resources are supported in inline_resource."
|
56
|
+
end
|
57
|
+
|
58
|
+
# Blatantly ripped off from chef/provider run_action
|
59
|
+
|
60
|
+
# TODO: it would be preferable to get the action to be executed in the
|
61
|
+
# constructor...
|
62
|
+
|
63
|
+
# user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode
|
64
|
+
inline_provider.load_current_resource
|
65
|
+
inline_provider.define_resource_requirements
|
66
|
+
inline_provider.process_resource_requirements
|
67
|
+
|
68
|
+
# user-defined providers including LWRPs may
|
69
|
+
# not include whyrun support - if they don't support it
|
70
|
+
# we can't execute any actions while we're running in
|
71
|
+
# whyrun mode. Instead we 'fake' whyrun by documenting that
|
72
|
+
# we can't execute the action.
|
73
|
+
# in non-whyrun mode, this will still cause the action to be
|
74
|
+
# executed normally.
|
75
|
+
if inline_provider.whyrun_supported? && !inline_provider.requirements.action_blocked?(@action)
|
76
|
+
inline_provider.send("action_#{inline_provider.action}")
|
77
|
+
elsif !inline_provider.whyrun_mode?
|
78
|
+
inline_provider.send("action_#{inline_provider.action}")
|
79
|
+
end
|
80
|
+
|
81
|
+
if inline_provider.resource_updated?
|
82
|
+
inline_provider.new_resource.updated_by_last_action(true)
|
83
|
+
end
|
84
|
+
|
85
|
+
inline_provider.cleanup_after_converge
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ChefMetal
|
2
|
+
class Machine
|
3
|
+
def initialize(node)
|
4
|
+
@node = node
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :node
|
8
|
+
|
9
|
+
# Sets up everything necessary for convergence to happen on the machine.
|
10
|
+
# The node MUST be saved as part of this procedure. Other than that,
|
11
|
+
# nothing is guaranteed except that converge() will work when this is done.
|
12
|
+
def setup_convergence(provider)
|
13
|
+
raise "setup_convergence not overridden on #{self.class}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def converge(provider)
|
17
|
+
raise "converge not overridden on #{self.class}"
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def execute(provider, command)
|
22
|
+
raise "execute not overridden on #{self.class}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def read_file(path)
|
26
|
+
raise "read_file not overridden on #{self.class}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def download_file(provider, path, local_path)
|
30
|
+
raise "read_file not overridden on #{self.class}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_file(provider, path, content)
|
34
|
+
raise "write_file not overridden on #{self.class}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def upload_file(provider, local_path, path)
|
38
|
+
raise "write_file not overridden on #{self.class}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_dir(provider, path)
|
42
|
+
raise "create_dir not overridden on #{self.class}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Delete file
|
46
|
+
def delete_file(provider, path)
|
47
|
+
raise "delete_file not overridden on #{self.class}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return true or false depending on whether file exists
|
51
|
+
def file_exists?(path)
|
52
|
+
raise "file_exists? not overridden on #{self.class}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return true or false depending on whether remote file differs from local path or content
|
56
|
+
def files_different?(path, local_path, content=nil)
|
57
|
+
raise "file_different? not overridden on #{self.class}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Set file attributes { mode, :owner, :group }
|
61
|
+
def set_attributes(provider, path, attributes)
|
62
|
+
raise "set_attributes not overridden on #{self.class}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get file attributes { :mode, :owner, :group }
|
66
|
+
def get_attributes(path)
|
67
|
+
raise "get_attributes not overridden on #{self.class}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Forward a remote port to local for the duration of the current connection
|
71
|
+
def forward_remote_port_to_local(remote_port, local_port)
|
72
|
+
raise "forward_remote_port_to_local not overridden on #{self.class}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def disconnect
|
76
|
+
raise "disconnect not overridden on #{self.class}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'chef_metal/machine'
|
2
|
+
|
3
|
+
module ChefMetal
|
4
|
+
class Machine
|
5
|
+
class BasicMachine < Machine
|
6
|
+
def initialize(node, transport, convergence_strategy)
|
7
|
+
super(node)
|
8
|
+
@transport = transport
|
9
|
+
@convergence_strategy = convergence_strategy
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :transport
|
13
|
+
attr_reader :convergence_strategy
|
14
|
+
|
15
|
+
# Sets up everything necessary for convergence to happen on the machine.
|
16
|
+
# The node MUST be saved as part of this procedure. Other than that,
|
17
|
+
# nothing is guaranteed except that converge() will work when this is done.
|
18
|
+
def setup_convergence(provider, machine_resource)
|
19
|
+
convergence_strategy.setup_convergence(provider, self, machine_resource)
|
20
|
+
end
|
21
|
+
|
22
|
+
def converge(provider)
|
23
|
+
convergence_strategy.converge(provider, self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute(provider, command)
|
27
|
+
provider.converge_by "run '#{command}' on #{node['name']}" do
|
28
|
+
transport.execute(command).error!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute_always(command)
|
33
|
+
transport.execute(command)
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_file(path)
|
37
|
+
transport.read_file(path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def download_file(provider, path, local_path)
|
41
|
+
if files_different?(path, local_path)
|
42
|
+
provider.converge_by "download file #{path} on #{node['name']} to #{local_path}" do
|
43
|
+
transport.download_file(path, local_path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_file(provider, path, content, options = {})
|
49
|
+
if files_different?(path, nil, content)
|
50
|
+
if options[:ensure_dir]
|
51
|
+
create_dir(provider, dirname_on_machine(path))
|
52
|
+
end
|
53
|
+
provider.converge_by "write file #{path} on #{node['name']}" do
|
54
|
+
transport.write_file(path, content)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def upload_file(provider, local_path, path, options = {})
|
60
|
+
if files_different?(path, local_path)
|
61
|
+
if options[:ensure_dir]
|
62
|
+
create_dir(provider, dirname_on_machine(path))
|
63
|
+
end
|
64
|
+
provider.converge_by "upload file #{local_path} to #{path} on #{node['name']}" do
|
65
|
+
transport.upload_file(local_path, path)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def forward_remote_port_to_local(remote_port, local_port)
|
71
|
+
transport.forward_remote_port_to_local(remote_port, local_port)
|
72
|
+
end
|
73
|
+
|
74
|
+
def disconnect
|
75
|
+
transport.disconnect
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|