chef-metal-vsphere 0.2.0 → 0.3.0.beta.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.
@@ -1,310 +0,0 @@
1
- require 'chef_metal/provisioner'
2
- require 'chef_metal/machine/windows_machine'
3
- require 'chef_metal/machine/unix_machine'
4
- require 'chef_metal/convergence_strategy/install_msi'
5
- require 'chef_metal/convergence_strategy/install_cached'
6
- require 'chef_metal/transport/ssh'
7
- require 'chef_metal_vsphere/version'
8
- require 'rbvmomi'
9
- require 'chef_metal_vsphere/vsphere_helpers'
10
-
11
- module ChefMetalVsphere
12
- # Provisions machines in vSphere.
13
- class VsphereProvisioner < ChefMetal::Provisioner
14
-
15
- include Chef::Mixin::ShellOut
16
- include ChefMetalVsphere::Helpers
17
-
18
- def self.inflate(node)
19
- url = node['normal']['provisioner_output']['provisioner_url']
20
- scheme, provider, id = url.split(':', 3)
21
- VsphereProvisioner.new({ :provider => provider }, id)
22
- end
23
-
24
- # Create a new Vsphere provisioner.
25
- #
26
- # ## Parameters
27
- # connect_options - hash of options to be passed to RbVmomi::VIM.connect
28
- # :vsphere_host - required - hostname of the vSphere API server
29
- # :vsphere_port - optional - port on the vSphere API server (default: 443)
30
- # :vshere_path - optional - path on the vSphere API server (default: /sdk)
31
- # :vsphere_ssl - optional - true to use ssl in connection to vSphere API server (default: true)
32
- # :vsphere_insecure - optional - true to ignore ssl certificate validation errors in connection to vSphere API server (default: false)
33
- # :vsphere_user - required - user name to use in connection to vSphere API server
34
- # :vsphere_password - required - password to use in connection to vSphere API server
35
- # :proxy_host - optional - http proxy host to use in connection to vSphere API server (default: none)
36
- # :proxy_port - optional - http proxy port to use in connection to vSphere API server (default: none)
37
- def initialize(connect_options)
38
- connect_options = stringify_keys(connect_options)
39
- default_connect_options = {
40
- 'vsphere_port' => 443,
41
- 'vsphere_ssl' => true,
42
- 'vsphere_insecure' => false,
43
- 'vsphere_path' => '/sdk'
44
- }
45
-
46
- @connect_options = default_connect_options.merge(connect_options)
47
-
48
- required_options = %w( vsphere_host vsphere_user vsphere_password )
49
- missing_options = []
50
- required_options.each do |opt|
51
- missing_options << opt unless @connect_options.has_key?(opt)
52
- end
53
- unless missing_options.empty?
54
- raise "missing required options: #{missing_options.join(', ')}"
55
- end
56
-
57
- # test vim connection
58
- vim || raise("cannot connect to [#{provisioner_url}]")
59
-
60
- @connect_options
61
- end
62
-
63
- attr_reader :connect_options
64
-
65
- # Acquire a machine, generally by provisioning it. Returns a Machine
66
- # object pointing at the machine, allowing useful actions like setup,
67
- # converge, execute, file and directory. The Machine object will have a
68
- # "node" property which must be saved to the server (if it is any
69
- # different from the original node object).
70
- #
71
- # ## Parameters
72
- # action_handler - the action_handler object that is calling this method; this
73
- # is generally a action_handler, but could be anything that can support the
74
- # ChefMetal::ActionHandler interface (i.e., in the case of the test
75
- # kitchen metal driver for acquiring and destroying VMs; see the base
76
- # class for what needs providing).
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: vsphere://host:port?ssl=[true|false]&insecure=[true|false]
85
- # -- bootstrap_options: hash of options to pass to RbVmomi::VIM::VirtualMachine::CloneTask()
86
- # :datacenter
87
- # :resource_pool
88
- # :cluster
89
- # :datastore
90
- # :template_name
91
- # :template_folder
92
- # :vm_folder
93
- # :winrm {...} (not yet implemented)
94
- # :ssh {...}
95
- #
96
- # Example bootstrap_options for vSphere:
97
- # TODO: add other CloneTask params, e.g.: datastore, annotation, resource_pool, ...
98
- # 'bootstrap_options' => {
99
- # 'template_name' =>'centos6.small',
100
- # 'template_folder' =>'Templates',
101
- # 'vm_folder' => 'MyApp'
102
- # }
103
- #
104
- # node['normal']['provisioner_output'] will be populated with information
105
- # about the created machine. For vSphere, it is a hash with this
106
- # format:
107
- #
108
- # -- provisioner_url: vsphere:host:port?ssl=[true|false]&insecure=[true|false]
109
- # -- vm_folder: name of the vSphere folder containing the VM
110
- #
111
- def acquire_machine(action_handler, node)
112
- # Set up the provisioner output
113
- provisioner_options = stringify_keys(node['normal']['provisioner_options'])
114
-
115
- vm_name = node['name']
116
- old_provisioner_output = node['normal']['provisioner_output']
117
- node['normal']['provisioner_output'] = provisioner_output = {
118
- 'provisioner_url' => provisioner_url,
119
- 'vm_name' => vm_name,
120
- 'bootstrap_options' => provisioner_options['bootstrap_options']
121
- }
122
-
123
- bootstrap_options = node['normal']['provisioner_output']['bootstrap_options']
124
- vm_folder = bootstrap_options['vm_folder']
125
-
126
- if bootstrap_options['ssh']
127
- wait_on_port = bootstrap_options['ssh']['port']
128
- raise "Must specify bootstrap_options[:ssh][:port]" if wait_on_port.nil?
129
- else
130
- raise 'bootstrapping is currently supported for ssh only'
131
- # wait_on_port = bootstrap_options['winrm']['port']
132
- end
133
-
134
- # TODO compare new options to existing and fail if we cannot change it
135
- # over (perhaps introduce a boolean that will force a delete and recreate
136
- # in such a case)
137
-
138
- vm = vm_instance(action_handler, node)
139
-
140
- unless vm_started?(vm, wait_on_port)
141
- action_handler.perform_action "Start VM and wait for port #{wait_on_port}" do
142
- start_vm(vm, wait_on_port)
143
- end
144
- end
145
-
146
- machine = machine_for(node)
147
-
148
- machine
149
- end
150
-
151
- # Connect to machine without acquiring it
152
- def connect_to_machine(node)
153
- machine_for(node)
154
- end
155
-
156
- def delete_machine(action_handler, node)
157
- if node['normal'] && node['normal']['provisioner_output']
158
- provisioner_output = node['normal']['provisioner_output']
159
- else
160
- provisioner_output = {}
161
- end
162
- vm_name = provisioner_output['vm_name'] || node['name']
163
- vm_folder = provisioner_output['bootstrap_options']['vm_folder']
164
- vm = vm_for(node)
165
-
166
- unless vm.nil?
167
- action_handler.perform_action "Delete VM [#{vm_folder}/#{vm_name}]" do
168
- vm.PowerOffVM_Task.wait_for_completion unless vm.runtime.powerState == 'poweredOff'
169
- vm.Destroy_Task.wait_for_completion
170
- end
171
- end
172
- end
173
-
174
- def stop_machine(action_handler, node)
175
- if node['normal'] && node['normal']['provisioner_output']
176
- provisioner_output = node['normal']['provisioner_output']
177
- else
178
- provisioner_output = {}
179
- end
180
- vm_name = provisioner_output['vm_name'] || node['name']
181
- vm_folder = provisioner_output['bootstrap_options']['vm_folder']
182
- vm = vm_for(node)
183
-
184
- unless vm_stopped?(vm)
185
- action_handler.perform_action "Shutdown guest OS and power off VM [#{vm_folder}/#{vm_name}]" do
186
- stop_vm(vm)
187
- end
188
- end
189
- end
190
-
191
- protected
192
-
193
- def provisioner_url
194
- "vsphere://#{connect_options['vsphere_host']}:#{connect_options['vsphere_port']}#{connect_options['vsphere_path']}?ssl=#{connect_options['vsphere_ssl']}&insecure=#{connect_options['vsphere_insecure']}"
195
- end
196
-
197
- def vm_instance(action_handler, node)
198
- bootstrap_options = node['normal']['provisioner_output']['bootstrap_options']
199
-
200
- datacenter = bootstrap_options['datacenter']
201
- vm_name = node['normal']['provisioner_output']['vm_name']
202
- vm_folder = bootstrap_options['vm_folder']
203
-
204
- vm = find_vm(datacenter, vm_folder, vm_name)
205
- return vm unless vm.nil?
206
-
207
- action_handler.perform_action "Clone a new VM instance from [#{bootstrap_options['template_folder']}/#{bootstrap_options['template_name']}]" do
208
- vm = clone_vm(vm_name, bootstrap_options)
209
- end
210
-
211
- vm
212
- end
213
-
214
- def clone_vm(vm_name, bootstrap_options)
215
- datacenter = bootstrap_options['datacenter']
216
- template_folder = bootstrap_options['template_folder']
217
- template_name = bootstrap_options['template_name']
218
-
219
- vm_template = find_vm(datacenter, template_folder, template_name) or raise("vSphere VM Template not found [#{template_folder}/#{template_name}]")
220
-
221
- vm = do_vm_clone(datacenter, vm_template, vm_name, bootstrap_options)
222
- end
223
-
224
- def machine_for(node)
225
- vm = vm_for(node) or raise "VM for node #{node['name']} has not been created!"
226
-
227
- if is_windows?(vm)
228
- ChefMetal::Machine::WindowsMachine.new(node, transport_for(node), convergence_strategy_for(node))
229
- else
230
- ChefMetal::Machine::UnixMachine.new(node, transport_for(node), convergence_strategy_for(node))
231
- end
232
- end
233
-
234
-
235
- def vm_for(node)
236
- bootstrap_options = node['normal']['provisioner_output']['bootstrap_options']
237
- datacenter = bootstrap_options['datacenter']
238
- vm_folder = bootstrap_options['vm_folder']
239
- vm_name = node['normal']['provisioner_output']['vm_name']
240
- vm = find_vm(datacenter, vm_folder, vm_name)
241
- vm
242
- end
243
-
244
- def is_windows?(vm)
245
- return false if vm.nil?
246
- vm.guest.guestFamily == 'windowsGuest'
247
- end
248
-
249
- def convergence_strategy_for(node)
250
- if is_windows?(vm_for(node))
251
- @windows_convergence_strategy ||= begin
252
- options = {}
253
- provisioner_options = node['normal']['provisioner_options'] || {}
254
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
255
- ChefMetal::ConvergenceStrategy::InstallMsi.new(options)
256
- end
257
- else
258
- @unix_convergence_strategy ||= begin
259
- options = {}
260
- provisioner_options = node['normal']['provisioner_options'] || {}
261
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
262
- ChefMetal::ConvergenceStrategy::InstallCached.new(options)
263
- end
264
- end
265
- end
266
-
267
- def transport_for(node)
268
- if is_windows?(vm_for(node))
269
- create_winrm_transport(node)
270
- else
271
- create_ssh_transport(node)
272
- end
273
- end
274
-
275
- def create_winrm_transport(node)
276
- raise 'Windows guest VMs are not yet supported'
277
- end
278
-
279
- def create_ssh_transport(node)
280
- bootstrap_options = node['normal']['provisioner_output']['bootstrap_options']
281
- vm = vm_for(node) or raise "VM for node #{node['name']} has not been created!"
282
-
283
- hostname = vm.guest.ipAddress
284
- ssh_user = bootstrap_options['ssh']['user']
285
- ssh_options = symbolize_keys(bootstrap_options['ssh'])
286
- transport_options = {
287
- :prefix => 'sudo '
288
- }
289
- ChefMetal::Transport::SSH.new(hostname, ssh_user, ssh_options, transport_options)
290
- end
291
-
292
- def stringify_keys(h)
293
- Hash === h ?
294
- Hash[
295
- h.map do |k, v|
296
- [k.respond_to?(:to_s) ? k.to_s : k, stringify_keys(v)]
297
- end
298
- ] : h
299
- end
300
-
301
- def symbolize_keys(h)
302
- Hash === h ?
303
- Hash[
304
- h.map do |k, v|
305
- [k.respond_to?(:to_sym) ? k.to_sym : k, symbolize_keys(v)]
306
- end
307
- ] : h
308
- end
309
- end
310
- end