chef-metal 0.1

Sign up to get free protection for your applications and to get access to all the features.
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,327 @@
1
+ require 'chef/mixin/shell_out'
2
+ require 'chef_metal/provisioner'
3
+
4
+ module ChefMetal
5
+ class Provisioner
6
+
7
+ # Provisions machines in vagrant.
8
+ class VagrantProvisioner < Provisioner
9
+
10
+ include Chef::Mixin::ShellOut
11
+
12
+ # Create a new vagrant provisioner.
13
+ #
14
+ # ## Parameters
15
+ # cluster_path - path to the directory containing the vagrant files, which
16
+ # should have been created with the vagrant_cluster resource.
17
+ def initialize(cluster_path)
18
+ @cluster_path = cluster_path
19
+ end
20
+
21
+ attr_reader :cluster_path
22
+
23
+ # Acquire a machine, generally by provisioning it. Returns a Machine
24
+ # object pointing at the machine, allowing useful actions like setup,
25
+ # converge, execute, file and directory. The Machine object will have a
26
+ # "node" property which must be saved to the server (if it is any
27
+ # different from the original node object).
28
+ #
29
+ # ## Parameters
30
+ # provider - the provider object that is calling this method.
31
+ # node - node object (deserialized json) representing this machine. If
32
+ # the node has a provisioner_options hash in it, these will be used
33
+ # instead of options provided by the provisioner. TODO compare and
34
+ # fail if different?
35
+ # node will have node['normal']['provisioner_options'] in it with any options.
36
+ # It is a hash with this format:
37
+ #
38
+ # -- provisioner_url: vagrant:<cluster_path>
39
+ # -- vagrant_options: hash of properties of the "config"
40
+ # object, i.e. "vm.box" => "ubuntu12" and "vm.box_url"
41
+ # -- vagrant_config: string containing other vagrant config.
42
+ # Should assume the variable "config" represents machine config.
43
+ # Will be written verbatim into the vm's Vagrantfile.
44
+ # -- transport_options: hash of options specifying the transport.
45
+ # :type => :ssh
46
+ # :type => :winrm
47
+ # If not specified, ssh is used unless vm.guest is :windows. If that is
48
+ # the case, the windows options are used and the port forward for 5985
49
+ # is detected.
50
+ # -- up_timeout: maximum time, in seconds, to wait for vagrant
51
+ # to bring up the machine. Defaults to 10 minutes.
52
+ #
53
+ # node['normal']['provisioner_output'] will be populated with information
54
+ # about the created machine. For vagrant, it is a hash with this
55
+ # format:
56
+ #
57
+ # -- provisioner_url: vagrant_cluster://<current_node>/<cluster_path>
58
+ # -- vm_name: name of vagrant vm created
59
+ # -- vm_file_path: path to machine-specific vagrant config file
60
+ # on disk
61
+ # -- forwarded_ports: hash with key as guest_port => host_port
62
+ #
63
+ def acquire_machine(provider, node)
64
+ # Set up the modified node data
65
+ provisioner_options = node['normal']['provisioner_options']
66
+ vm_name = node['name']
67
+ old_provisioner_output = node['normal']['provisioner_output']
68
+ node['normal']['provisioner_output'] = provisioner_output = {
69
+ 'provisioner_url' => provisioner_url(provider),
70
+ 'vm_name' => vm_name,
71
+ 'vm_file_path' => File.join(cluster_path, "#{vm_name}.vm")
72
+ }
73
+ # Preserve existing forwarded ports
74
+ provisioner_output['forwarded_ports'] = old_provisioner_output['forwarded_ports'] if old_provisioner_output
75
+
76
+ # TODO compare new options to existing and fail if we cannot change it
77
+ # over (perhaps introduce a boolean that will force a delete and recreate
78
+ # in such a case)
79
+
80
+ # Determine contents of vm file
81
+ vm_file_content = "Vagrant.configure('2') do |outer_config|\n"
82
+ vm_file_content << " outer_config.vm.define #{vm_name.inspect} do |config|\n"
83
+ merged_vagrant_options = { 'vm.hostname' => node['name'] }
84
+ merged_vagrant_options.merge!(provisioner_options['vagrant_options']) if provisioner_options['vagrant_options']
85
+ merged_vagrant_options.each_pair do |key, value|
86
+ vm_file_content << " config.#{key} = #{value.inspect}\n"
87
+ end
88
+ vm_file_content << provisioner_options['vagrant_config'] if provisioner_options['vagrant_config']
89
+ vm_file_content << " end\nend\n"
90
+
91
+ # Set up vagrant file
92
+ vm_file = ChefMetal.inline_resource(provider) do
93
+ file provisioner_output['vm_file_path'] do
94
+ content vm_file_content
95
+ action :create
96
+ end
97
+ end
98
+
99
+ # Check current status of vm
100
+ current_status = vagrant_status(vm_name)
101
+ up_timeout = provisioner_options['up_timeout'] || 10*60
102
+
103
+ if current_status != 'running'
104
+ # Run vagrant up if vm is not running
105
+ provider.converge_by "run vagrant up #{vm_name} (status was '#{current_status}')" do
106
+ result = shell_out("vagrant up #{vm_name}", :cwd => cluster_path, :timeout => up_timeout)
107
+ if result.exitstatus != 0
108
+ raise "vagrant up #{vm_name} failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
109
+ end
110
+ parse_vagrant_up(result.stdout, node)
111
+ end
112
+ elsif vm_file.updated_by_last_action?
113
+ # Run vagrant reload if vm is running and vm file changed
114
+ provider.converge_by "run vagrant reload #{vm_name}" do
115
+ result = shell_out("vagrant reload #{vm_name}", :cwd => cluster_path, :timeout => up_timeout)
116
+ if result.exitstatus != 0
117
+ raise "vagrant reload #{vm_name} failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
118
+ end
119
+ parse_vagrant_up(result.stdout, node)
120
+ end
121
+ end
122
+
123
+ # Create machine object for callers to use
124
+ machine_for(node)
125
+ end
126
+
127
+ # Connect to machine without acquiring it
128
+ def connect_to_machine(node)
129
+ machine_for(node)
130
+ end
131
+
132
+ def delete_machine(provider, node)
133
+ if node['normal'] && node['normal']['provisioner_output']
134
+ provisioner_output = node['normal']['provisioner_output']
135
+ else
136
+ provisioner_output = {}
137
+ end
138
+ vm_name = provisioner_output['vm_name'] || node['name']
139
+ current_status = vagrant_status(vm_name)
140
+ if current_status != 'not created'
141
+ provider.converge_by "run vagrant destroy -f #{vm_name} (status was '#{current_status}')" do
142
+ result = shell_out("vagrant destroy -f #{vm_name}", :cwd => cluster_path)
143
+ if result.exitstatus != 0
144
+ raise "vagrant destroy failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
145
+ end
146
+ end
147
+ end
148
+
149
+ convergence_strategy_for(node).delete_chef_objects(provider, node)
150
+
151
+ vm_file_path = provisioner_output['vm_file_path'] || File.join(cluster_path, "#{vm_name}.vm")
152
+ ChefMetal.inline_resource(provider) do
153
+ file vm_file_path do
154
+ action :delete
155
+ end
156
+ end
157
+ end
158
+
159
+ def stop_machine(provider, node)
160
+ if node['normal'] && node['normal']['provisioner_output']
161
+ provisioner_output = node['normal']['provisioner_output']
162
+ else
163
+ provisioner_output = {}
164
+ end
165
+ vm_name = provisioner_output['vm_name'] || node['name']
166
+ current_status = vagrant_status(vm_name)
167
+ if current_status == 'running'
168
+ provider.converge_by "run vagrant halt #{vm_name} (status was '#{current_status}')" do
169
+ result = shell_out("vagrant halt #{vm_name}", :cwd => cluster_path)
170
+ if result.exitstatus != 0
171
+ raise "vagrant halt failed!\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+
178
+ # Used by vagrant_cluster and machine to get the string used to configure vagrant
179
+ def self.vagrant_config_string(vagrant_config, variable, line_prefix)
180
+ hostname = name.gsub(/[^A-Za-z0-9\-]/, '-')
181
+
182
+ result = ''
183
+ vagrant_config.each_pair do |key, value|
184
+ result += "#{line_prefix}#{variable}.#{key} = #{value.inspect}\n"
185
+ end
186
+ result
187
+ end
188
+
189
+ protected
190
+
191
+ def provisioner_url(provider)
192
+ "vagrant_cluster://#{provider.node['name']}#{cluster_path}"
193
+ end
194
+
195
+ def parse_vagrant_up(output, node)
196
+ # Grab forwarded port info
197
+ in_forwarding_ports = false
198
+ output.lines.each do |line|
199
+ if in_forwarding_ports
200
+ if line =~ /-- (\d+) => (\d+)/
201
+ node['normal']['provisioner_output']['forwarded_ports'][$1] = $2
202
+ else
203
+ in_forwarding_ports = false
204
+ end
205
+ elsif line =~ /Forwarding ports...$/
206
+ node['normal']['provisioner_output']['forwarded_ports'] = {}
207
+ in_forwarding_ports = true
208
+ end
209
+ end
210
+ end
211
+
212
+ def machine_for(node)
213
+ if vagrant_option(node, 'vm.guest').to_s == 'windows'
214
+ require 'chef_metal/machine/windows_machine'
215
+ ChefMetal::Machine::WindowsMachine.new(node, transport_for(node), convergence_strategy_for(node))
216
+ else
217
+ require 'chef_metal/machine/unix_machine'
218
+ ChefMetal::Machine::UnixMachine.new(node, transport_for(node), convergence_strategy_for(node))
219
+ end
220
+ end
221
+
222
+ def convergence_strategy_for(node)
223
+ if vagrant_option(node, 'vm.guest').to_s == 'windows'
224
+ require 'chef_metal/convergence_strategy/install_msi'
225
+ ChefMetal::ConvergenceStrategy::InstallMsi.new
226
+ else
227
+ require 'chef_metal/convergence_strategy/install_sh'
228
+ ChefMetal::ConvergenceStrategy::InstallSh.new
229
+ end
230
+ end
231
+
232
+ def transport_for(node)
233
+ if vagrant_option(node, 'vm.guest').to_s == 'windows'
234
+ create_winrm_transport(node)
235
+ else
236
+ create_ssh_transport(node)
237
+ end
238
+ end
239
+
240
+ def vagrant_option(node, option)
241
+ if node['normal']['provisioner_options'] &&
242
+ node['normal']['provisioner_options']['vagrant_options']
243
+ node['normal']['provisioner_options']['vagrant_options'][option]
244
+ else
245
+ nil
246
+ end
247
+ end
248
+
249
+ def vagrant_status(name)
250
+ status_output = shell_out("vagrant status #{name}", :cwd => cluster_path).stdout
251
+ if status_output =~ /^#{name}\s+([^\n]+)\s+\(([^\n]+)\)$/m
252
+ $1
253
+ else
254
+ 'not created'
255
+ end
256
+ end
257
+
258
+ def create_winrm_transport(node)
259
+ require 'chef_metal/transport/winrm'
260
+
261
+ provisioner_output = node['default']['provisioner_output'] || {}
262
+ forwarded_ports = provisioner_output['forwarded_ports'] || {}
263
+
264
+ # TODO IPv6 loopback? What do we do for that?
265
+ hostname = vagrant_option(node, 'winrm.host') || '127.0.0.1'
266
+ port = vagrant_option(node, 'winrm.port') || forwarded_ports[5985] || 5985
267
+ endpoint = "http://#{hostname}:#{port}/wsman"
268
+ type = :plaintext
269
+ options = {
270
+ :user => vagrant_option(node, 'winrm.username') || 'vagrant',
271
+ :pass => vagrant_option(node, 'winrm.password') || 'vagrant',
272
+ :disable_sspi => true
273
+ }
274
+
275
+ ChefMetal::Transport::WinRM.new(endpoint, type, options)
276
+ end
277
+
278
+ def create_ssh_transport(node)
279
+ require 'chef_metal/transport/ssh'
280
+
281
+ vagrant_ssh_config = vagrant_ssh_config_for(node)
282
+ hostname = vagrant_ssh_config['HostName']
283
+ username = vagrant_ssh_config['User']
284
+ ssh_options = {
285
+ :port => vagrant_ssh_config['Port'],
286
+ :auth_methods => ['publickey'],
287
+ :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
288
+ :paranoid => yes_or_no(vagrant_ssh_config['StrictHostKeyChecking']),
289
+ :keys => [ strip_quotes(vagrant_ssh_config['IdentityFile']) ],
290
+ :keys_only => yes_or_no(vagrant_ssh_config['IdentitiesOnly'])
291
+ }
292
+ ssh_options[:auth_methods] = %w(password) if yes_or_no(vagrant_ssh_config['PasswordAuthentication'])
293
+ options = {
294
+ :prefix => 'sudo '
295
+ }
296
+ ChefMetal::Transport::SSH.new(hostname, username, ssh_options, options)
297
+ end
298
+
299
+ def vagrant_ssh_config_for(node)
300
+ vagrant_ssh_config = {}
301
+ result = shell_out("vagrant ssh-config #{node['normal']['provisioner_output']['vm_name']}", :cwd => cluster_path)
302
+ result.stdout.lines.inject({}) do |result, line|
303
+ line =~ /^\s*(\S+)\s+(.+)/
304
+ vagrant_ssh_config[$1] = $2
305
+ end
306
+ vagrant_ssh_config
307
+ end
308
+
309
+ def yes_or_no(str)
310
+ case str
311
+ when 'yes'
312
+ true
313
+ else
314
+ false
315
+ end
316
+ end
317
+
318
+ def strip_quotes(str)
319
+ if str[0] == '"' && str[-1] == '"' && str.size >= 2
320
+ str[1..-2]
321
+ else
322
+ str
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,26 @@
1
+ require 'chef_metal'
2
+ require 'chef_metal/provisioner/fog_provisioner'
3
+
4
+ class Chef
5
+ class Recipe
6
+ def with_provisioner(provisioner, &block)
7
+ ChefMetal.with_provisioner(provisioner, &block)
8
+ end
9
+
10
+ def with_provisioner_options(provisioner_options, &block)
11
+ ChefMetal.with_provisioner_options(provisioner_options, &block)
12
+ end
13
+
14
+ def with_vagrant_cluster(cluster_path, &block)
15
+ ChefMetal.with_vagrant_cluster(cluster_path, &block)
16
+ end
17
+
18
+ def with_vagrant_box(box_name, vagrant_options = {}, &block)
19
+ ChefMetal.with_vagrant_box(box_name, vagrant_options, &block)
20
+ end
21
+
22
+ def with_fog_ec2_provisioner(options = {}, &block)
23
+ ChefMetal.with_provisioner(ChefMetal::Provisioner::FogProvisioner.new({ :provider => 'AWS' }.merge(options)), &block)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ module ChefMetal
2
+ class Transport
3
+ def execute(command)
4
+ raise "execute not overridden on #{self.class}"
5
+ end
6
+
7
+ def read_file(path)
8
+ raise "read_file not overridden on #{self.class}"
9
+ end
10
+
11
+ def write_file(path, content)
12
+ raise "write_file not overridden on #{self.class}"
13
+ end
14
+
15
+ def download_file(path, local_path)
16
+ IO.write(local_path, read_file(path))
17
+ end
18
+
19
+ def upload_file(local_path, path)
20
+ write_file(path, IO.read(local_path))
21
+ end
22
+
23
+ # Forward requests to a port on the guest to a server on the host
24
+ def forward_remote_port_to_local(remote_port, local_port)
25
+ raise "forward_remote_port_to_local not overridden on #{self.class}"
26
+ end
27
+
28
+ def disconnect
29
+ raise "disconnect not overridden on #{self.class}"
30
+ end
31
+
32
+ def available?
33
+ raise "available? not overridden on #{self.class}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,157 @@
1
+ require 'chef_metal/transport'
2
+
3
+ module ChefMetal
4
+ class Transport
5
+ class SSH < Transport
6
+ def initialize(host, username, ssh_options, options)
7
+ require 'net/ssh'
8
+ require 'net/scp'
9
+ @host = host
10
+ @username = username
11
+ @ssh_options = ssh_options
12
+ @options = options
13
+ end
14
+
15
+ attr_reader :host
16
+ attr_reader :username
17
+ attr_reader :ssh_options
18
+ attr_reader :options
19
+
20
+ def execute(command)
21
+ Chef::Log.info("Executing #{command} on #{username}@#{host}")
22
+ stdout = ''
23
+ stderr = ''
24
+ exitstatus = nil
25
+ channel = session.open_channel do |channel|
26
+ channel.exec("#{options[:prefix]}#{command}") do |ch, success|
27
+ raise "could not execute command: #{command.inspect}" unless success
28
+
29
+ channel.on_data do |ch2, data|
30
+ stdout << data
31
+ end
32
+
33
+ channel.on_extended_data do |ch2, type, data|
34
+ stderr << data
35
+ end
36
+
37
+ channel.on_request "exit-status" do |ch, data|
38
+ exitstatus = data.read_long
39
+ end
40
+ end
41
+ end
42
+
43
+ channel.wait
44
+
45
+ Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
46
+ Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != ''
47
+ Chef::Log.info("Stderr was:\n#{stderr}") if stderr != ''
48
+ SSHResult.new(stdout, stderr, exitstatus)
49
+ end
50
+
51
+ def read_file(path)
52
+ Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
53
+ result = StringIO.new
54
+ download(path, result)
55
+ result.string
56
+ end
57
+
58
+ def download_file(path, local_path)
59
+ Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
60
+ download(path, local_path)
61
+ end
62
+
63
+ def write_file(path, content)
64
+ if options[:prefix]
65
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
66
+ remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
67
+ Chef::Log.debug("Writing #{content.length} bytes to #{remote_tempfile} on #{username}@#{host}")
68
+ Net::SCP.new(session).upload!(StringIO.new(content), remote_tempfile)
69
+ execute("mv #{remote_tempfile} #{path}")
70
+ else
71
+ Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
72
+ Net::SCP.new(session).upload!(StringIO.new(content), path)
73
+ end
74
+ end
75
+
76
+ def upload_file(local_path, path)
77
+ if options[:prefix]
78
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
79
+ remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
80
+ Chef::Log.debug("Uploading #{local_path} to #{remote_tempfile} on #{username}@#{host}")
81
+ Net::SCP.new(session).upload!(local_path, remote_tempfile)
82
+ execute("mv #{remote_tempfile} #{path}")
83
+ else
84
+ Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
85
+ Net::SCP.new(session).upload!(local_path, path)
86
+ end
87
+ end
88
+
89
+ def forward_remote_port_to_local(remote_port, local_port)
90
+ # TODO IPv6
91
+ Chef::Log.debug("Forwarding local server 127.0.0.1:#{local_port} to port #{remote_port} on #{username}@#{host}")
92
+ session.forward.remote(local_port, "127.0.0.1", remote_port)
93
+ end
94
+
95
+ def disconnect
96
+ if @session
97
+ begin
98
+ Chef::Log.debug("Closing SSH session on #{username}@#{host}")
99
+ @session.close
100
+ rescue
101
+ end
102
+ @session = nil
103
+ end
104
+ end
105
+
106
+ def available?
107
+ execute('pwd')
108
+ true
109
+ rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::AuthenticationFailed, Net::SSH::Disconnect, Net::SSH::HostKeyMismatch
110
+ Chef::Log.debug("#{username}@#{host} unavailable: could not execute 'pwd' on #{host}: #{$!.inspect}")
111
+ false
112
+ end
113
+
114
+ protected
115
+
116
+ def session
117
+ @session ||= begin
118
+ Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_options.inspect}")
119
+ Net::SSH.start(host, username, ssh_options)
120
+ end
121
+ end
122
+
123
+ def download(path, local_path)
124
+ channel = Net::SCP.new(session).download(path, local_path)
125
+ begin
126
+ channel.wait
127
+ rescue Net::SCP::Error
128
+ nil
129
+ rescue
130
+ # This works around https://github.com/net-ssh/net-scp/pull/10 until a new net-scp is merged.
131
+ begin
132
+ channel.close
133
+ channel.wait
134
+ rescue Net::SCP::Error
135
+ nil
136
+ end
137
+ end
138
+ end
139
+
140
+ class SSHResult
141
+ def initialize(stdout, stderr, exitstatus)
142
+ @stdout = stdout
143
+ @stderr = stderr
144
+ @exitstatus = exitstatus
145
+ end
146
+
147
+ attr_reader :stdout
148
+ attr_reader :stderr
149
+ attr_reader :exitstatus
150
+
151
+ def error!
152
+ raise "Error: code #{exitstatus}.\nSTDOUT:#{stdout}\nSTDERR:#{stderr}" if exitstatus != 0
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end