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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +160 -0
  4. data/Rakefile +6 -0
  5. data/lib/chef/provider/fog_key_pair.rb +106 -0
  6. data/lib/chef/provider/machine.rb +60 -0
  7. data/lib/chef/provider/machine_file.rb +39 -0
  8. data/lib/chef/provider/vagrant_box.rb +44 -0
  9. data/lib/chef/provider/vagrant_cluster.rb +39 -0
  10. data/lib/chef/resource/fog_key_pair.rb +34 -0
  11. data/lib/chef/resource/machine.rb +56 -0
  12. data/lib/chef/resource/machine_file.rb +25 -0
  13. data/lib/chef/resource/vagrant_box.rb +18 -0
  14. data/lib/chef/resource/vagrant_cluster.rb +16 -0
  15. data/lib/chef_metal.rb +84 -0
  16. data/lib/chef_metal/aws_credentials.rb +55 -0
  17. data/lib/chef_metal/convergence_strategy.rb +15 -0
  18. data/lib/chef_metal/convergence_strategy/install_msi.rb +41 -0
  19. data/lib/chef_metal/convergence_strategy/install_sh.rb +36 -0
  20. data/lib/chef_metal/convergence_strategy/precreate_chef_objects.rb +140 -0
  21. data/lib/chef_metal/inline_resource.rb +88 -0
  22. data/lib/chef_metal/machine.rb +79 -0
  23. data/lib/chef_metal/machine/basic_machine.rb +79 -0
  24. data/lib/chef_metal/machine/unix_machine.rb +108 -0
  25. data/lib/chef_metal/machine/windows_machine.rb +94 -0
  26. data/lib/chef_metal/provisioner.rb +71 -0
  27. data/lib/chef_metal/provisioner/fog_provisioner.rb +378 -0
  28. data/lib/chef_metal/provisioner/vagrant_provisioner.rb +327 -0
  29. data/lib/chef_metal/recipe_dsl.rb +26 -0
  30. data/lib/chef_metal/transport.rb +36 -0
  31. data/lib/chef_metal/transport/ssh.rb +157 -0
  32. data/lib/chef_metal/transport/winrm.rb +91 -0
  33. data/lib/chef_metal/version.rb +3 -0
  34. 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