knife-vsphere 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,486 +1,484 @@
1
- #
2
- # Author:: Ezra Pagel (<ezra@cpan.org>)
3
- # Contributor:: Jesse Campbell (<hikeit@gmail.com>)
4
- # Contributor:: Bethany Erskine (<bethany@paperlesspost.com>)
5
- # Contributor:: Adrian Stanila (https://github.com/sacx)
6
- # License:: Apache License, Version 2.0
7
- #
8
-
9
- require 'chef/knife'
10
- require 'chef/knife/base_vsphere_command'
11
- require 'rbvmomi'
12
- require 'netaddr'
13
-
14
- # Clone an existing template into a new VM, optionally applying a customization specification.
15
- # usage:
16
- # knife vsphere vm clone NewNode UbuntuTemplate --cspec StaticSpec \
17
- # --cips 192.168.0.99/24,192.168.1.99/24 \
18
- # --chostname NODENAME --cdomain NODEDOMAIN
19
- class Chef::Knife::VsphereVmClone < Chef::Knife::BaseVsphereCommand
20
-
21
- banner "knife vsphere vm clone VMNAME (options)"
22
-
23
- get_common_options
24
-
25
- option :dest_folder,
26
- :long => "--dest-folder FOLDER",
27
- :description => "The folder into which to put the cloned VM"
28
-
29
- option :datastore,
30
- :long => "--datastore STORE",
31
- :description => "The datastore into which to put the cloned VM"
32
-
33
- option :resource_pool,
34
- :long => "--resource-pool POOL",
35
- :description => "The resource pool into which to put the cloned VM"
36
-
37
- option :source_vm,
38
- :long => "--template TEMPLATE",
39
- :description => "The source VM / Template to clone from",
40
- :required => true
41
-
42
- option :annotation,
43
- :long => "--annotation TEXT",
44
- :description => "Add TEXT in Notes field from annotation"
45
-
46
- option :customization_spec,
47
- :long => "--cspec CUST_SPEC",
48
- :description => "The name of any customization specification to apply"
49
-
50
- option :customization_plugin,
51
- :long => "--cplugin CUST_PLUGIN_PATH",
52
- :description => "Path to plugin that implements KnifeVspherePlugin.customize_clone_spec and/or KnifeVspherePlugin.reconfig_vm"
53
-
54
- option :customization_plugin_data,
55
- :long => "--cplugin-data CUST_PLUGIN_DATA",
56
- :description => "String of data to pass to the plugin. Use any format you wish."
57
-
58
- option :customization_vlan,
59
- :long => "--cvlan CUST_VLAN",
60
- :description => "VLAN name for network adapter to join"
61
-
62
- option :customization_ips,
63
- :long => "--cips CUST_IPS",
64
- :description => "Comma-delimited list of CIDR IPs for customization"
65
-
66
- option :customization_dns_ips,
67
- :long => "--cdnsips CUST_DNS_IPS",
68
- :description => "Comma-delimited list of DNS IP addresses"
69
-
70
- option :customization_dns_suffixes,
71
- :long => "--cdnssuffix CUST_DNS_SUFFIXES",
72
- :description => "Comma-delimited list of DNS search suffixes"
73
-
74
- option :customization_gw,
75
- :long => "--cgw CUST_GW",
76
- :description => "CIDR IP of gateway for customization"
77
-
78
- option :customization_hostname,
79
- :long => "--chostname CUST_HOSTNAME",
80
- :description => "Unqualified hostname for customization"
81
-
82
- option :customization_domain,
83
- :long => "--cdomain CUST_DOMAIN",
84
- :description => "Domain name for customization"
85
-
86
- option :customization_tz,
87
- :long => "--ctz CUST_TIMEZONE",
88
- :description => "Timezone invalid 'Area/Location' format"
89
-
90
- option :customization_cpucount,
91
- :long => "--ccpu CUST_CPU_COUNT",
92
- :description => "Number of CPUs"
93
-
94
- option :customization_memory,
95
- :long => "--cram CUST_MEMORY_GB",
96
- :description => "Gigabytes of RAM"
97
-
98
- option :power,
99
- :long => "--start",
100
- :description => "Indicates whether to start the VM after a successful clone",
101
- :boolean => false
102
-
103
- option :bootstrap,
104
- :long => "--bootstrap",
105
- :description => "Indicates whether to bootstrap the VM",
106
- :boolean => false
107
-
108
- option :fqdn,
109
- :long => "--fqdn SERVER_FQDN",
110
- :description => "Fully qualified hostname for bootstrapping"
111
-
112
- option :ssh_user,
113
- :short => "-x USERNAME",
114
- :long => "--ssh-user USERNAME",
115
- :description => "The ssh username"
116
- $default[:ssh_user] = "root"
117
-
118
- option :ssh_password,
119
- :short => "-P PASSWORD",
120
- :long => "--ssh-password PASSWORD",
121
- :description => "The ssh password"
122
-
123
- option :ssh_port,
124
- :short => "-p PORT",
125
- :long => "--ssh-port PORT",
126
- :description => "The ssh port"
127
- $default[:ssh_port] = 22
128
-
129
- option :identity_file,
130
- :short => "-i IDENTITY_FILE",
131
- :long => "--identity-file IDENTITY_FILE",
132
- :description => "The SSH identity file used for authentication"
133
-
134
- option :chef_node_name,
135
- :short => "-N NAME",
136
- :long => "--node-name NAME",
137
- :description => "The Chef node name for your new node"
138
-
139
- option :prerelease,
140
- :long => "--prerelease",
141
- :description => "Install the pre-release chef gems",
142
- :boolean => false
143
-
144
- option :bootstrap_version,
145
- :long => "--bootstrap-version VERSION",
146
- :description => "The version of Chef to install",
147
- :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
148
-
149
- option :bootstrap_proxy,
150
- :long => "--bootstrap-proxy PROXY_URL",
151
- :description => "The proxy server for the node being bootstrapped",
152
- :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
153
-
154
- option :distro,
155
- :short => "-d DISTRO",
156
- :long => "--distro DISTRO",
157
- :description => "Bootstrap a distro using a template"
158
-
159
- option :template_file,
160
- :long => "--template-file TEMPLATE",
161
- :description => "Full path to location of template to use"
162
-
163
- option :run_list,
164
- :short => "-r RUN_LIST",
165
- :long => "--run-list RUN_LIST",
166
- :description => "Comma separated list of roles/recipes to apply"
167
- $default[:run_list] = ''
168
-
169
- option :secret_file,
170
- :long => "--secret-file SECRET_FILE",
171
- :description => "A file containing the secret key to use to encrypt data bag item values"
172
- $default[:secret_file] = ''
173
-
174
- option :no_host_key_verify,
175
- :long => "--no-host-key-verify",
176
- :description => "Disable host key verification",
177
- :boolean => true
178
-
179
- option :first_boot_attributes,
180
- :short => "-j JSON_ATTRIBS",
181
- :long => "--json-attributes",
182
- :description => "A JSON string to be added to the first run of chef-client",
183
- :proc => lambda { |o| JSON.parse(o) },
184
- :default => {}
185
-
186
- option :disable_customization,
187
- :long => "--disable-customization",
188
- :description => "Disable default customization",
189
- :boolean => true,
190
- :default => false
191
-
192
- option :log_level,
193
- :short => "-l LEVEL",
194
- :long => "--log_level",
195
- :description => "Set the log level (debug, info, warn, error, fatal) for chef-client",
196
- :proc => lambda { |l| l.to_sym }
197
-
198
- def run
199
- $stdout.sync = true
200
-
201
- vmname = @name_args[0]
202
- if vmname.nil?
203
- show_usage
204
- fatal_exit("You must specify a virtual machine name")
205
- end
206
- config[:chef_node_name] = vmname unless config[:chef_node_name]
207
- config[:vmname] = vmname
208
-
209
- if get_config(:bootstrap) && get_config(:distro) && !@@chef_config_dir
210
- fatal_exit("Can't find .chef for bootstrap files. chdir to a location with a .chef directory and try again")
211
- end
212
-
213
- vim = get_vim_connection
214
-
215
- dcname = get_config(:vsphere_dc)
216
- dc = vim.serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
217
-
218
- src_folder = find_folder(get_config(:folder)) || dc.vmFolder
219
-
220
- src_vm = find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, config[:source_vm]) or
221
- abort "VM/Template not found"
222
-
223
- clone_spec = generate_clone_spec(src_vm.config)
224
-
225
- cust_folder = config[:dest_folder] || get_config(:folder)
226
-
227
- dest_folder = cust_folder.nil? ? src_vm.vmFolder : find_folder(cust_folder)
228
-
229
- task = src_vm.CloneVM_Task(:folder => dest_folder, :name => vmname, :spec => clone_spec)
230
- puts "Cloning template #{config[:source_vm]} to new VM #{vmname}"
231
- task.wait_for_completion
232
- puts "Finished creating virtual machine #{vmname}"
233
-
234
- if customization_plugin && customization_plugin.respond_to?(:reconfig_vm)
235
- target_vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or abort "VM could not be found in #{dest_folder}"
236
- customization_plugin.reconfig_vm(target_vm)
237
- end
238
-
239
- if get_config(:power) || get_config(:bootstrap)
240
- vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or
241
- fatal_exit("VM #{vmname} not found")
242
- vm.PowerOnVM_Task.wait_for_completion
243
- puts "Powered on virtual machine #{vmname}"
244
- end
245
-
246
- if get_config(:bootstrap)
247
- sleep 2 until vm.guest.ipAddress
248
- config[:fqdn] = vm.guest.ipAddress unless config[:fqdn]
249
- print "Waiting for sshd..."
250
- print "." until tcp_test_ssh(config[:fqdn])
251
- puts "done"
252
-
253
- bootstrap_for_node.run
254
- end
255
- end
256
-
257
- # Builds a CloneSpec
258
- def generate_clone_spec (src_config)
259
-
260
- rspec = nil
261
- if get_config(:resource_pool)
262
- rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => find_pool(get_config(:resource_pool)))
263
- else
264
- dcname = get_config(:vsphere_dc)
265
- dc = config[:vim].serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
266
- hosts = find_all_in_folder(dc.hostFolder, RbVmomi::VIM::ComputeResource)
267
- rp = hosts.first.resourcePool
268
- rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => rp)
269
- end
270
-
271
- if get_config(:datastore)
272
- rspec.datastore = find_datastore(get_config(:datastore))
273
- end
274
-
275
- clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
276
- :powerOn => false,
277
- :template => false)
278
-
279
- clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new)
280
-
281
- if get_config(:annotation)
282
- clone_spec.config.annotation = get_config(:annotation)
283
- end
284
-
285
- if get_config(:customization_cpucount)
286
- clone_spec.config.numCPUs = get_config(:customization_cpucount)
287
- end
288
-
289
- if get_config(:customization_memory)
290
- clone_spec.config.memoryMB = Integer(get_config(:customization_memory)) * 1024
291
- end
292
-
293
- if get_config(:customization_vlan)
294
- network = find_network(get_config(:customization_vlan))
295
- card = src_config.hardware.device.find { |d| d.deviceInfo.label == "Network adapter 1" } or
296
- abort "Can't find source network card to customize"
297
- begin
298
- switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(:switchUuid => network.config.distributedVirtualSwitch.uuid, :portgroupKey => network.key)
299
- card.backing.port = switch_port
300
- rescue
301
- # not connected to a distibuted switch?
302
- card.backing.deviceName = network.name
303
- end
304
- dev_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit")
305
- clone_spec.config.deviceChange.push dev_spec
306
- end
307
-
308
- if get_config(:customization_spec)
309
- csi = find_customization(get_config(:customization_spec)) or
310
- fatal_exit("failed to find customization specification named #{get_config(:customization_spec)}")
311
-
312
- if csi.info.type != "Linux"
313
- fatal_exit("Only Linux customization specifications are currently supported")
314
- end
315
- cust_spec = csi.spec
316
- else
317
- global_ipset = RbVmomi::VIM.CustomizationGlobalIPSettings
318
- cust_spec = RbVmomi::VIM.CustomizationSpec(:globalIPSettings => global_ipset)
319
- end
320
-
321
- if get_config(:customization_dns_ips)
322
- cust_spec.globalIPSettings.dnsServerList = get_config(:customization_dns_ips).split(',')
323
- end
324
-
325
- if get_config(:customization_dns_suffixes)
326
- cust_spec.globalIPSettings.dnsSuffixList = get_config(:customization_dns_suffixes).split(',')
327
- end
328
-
329
- if config[:customization_ips]
330
- if get_config(:customization_gw)
331
- cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i, get_config(:customization_gw)) }
332
- else
333
- cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
334
- end
335
- end
336
-
337
- unless get_config(:disable_customization)
338
- use_ident = !config[:customization_hostname].nil? || !get_config(:customization_domain).nil? || cust_spec.identity.nil?
339
-
340
-
341
- if use_ident
342
- # TODO - verify that we're deploying a linux spec, at least warn
343
- ident = RbVmomi::VIM.CustomizationLinuxPrep
344
-
345
- ident.hostName = RbVmomi::VIM.CustomizationFixedName
346
- if config[:customization_hostname]
347
- ident.hostName.name = config[:customization_hostname]
348
- else
349
- ident.hostName.name = config[:vmname]
350
- end
351
-
352
- if get_config(:customization_domain)
353
- ident.domain = get_config(:customization_domain)
354
- else
355
- ident.domain = ''
356
- end
357
-
358
- cust_spec.identity = ident
359
- end
360
-
361
- if customization_plugin && customization_plugin.respond_to?(:customize_clone_spec)
362
- clone_spec = customization_plugin.customize_clone_spec(src_config, clone_spec)
363
- end
364
-
365
- clone_spec.customization = cust_spec
366
- end
367
- clone_spec
368
- end
369
-
370
- # Loads the customization plugin if one was specified
371
- # @return [KnifeVspherePlugin] the loaded and initialized plugin or nil
372
- def customization_plugin
373
- if @customization_plugin.nil?
374
- if cplugin_path = get_config(:customization_plugin)
375
- if File.exists? cplugin_path
376
- require cplugin_path
377
- else
378
- abort "Customization plugin could not be found at #{cplugin_path}"
379
- end
380
-
381
- if Object.const_defined? 'KnifeVspherePlugin'
382
- @customization_plugin = Object.const_get('KnifeVspherePlugin').new
383
- if cplugin_data = get_config(:customization_plugin_data)
384
- if @customization_plugin.respond_to?(:data=)
385
- @customization_plugin.data = cplugin_data
386
- else
387
- abort "Customization plugin has no :data= accessor to receive the --cplugin-data argument. Define both or neither."
388
- end
389
- end
390
- else
391
- abort "KnifeVspherePlugin class is not defined in #{cplugin_path}"
392
- end
393
- end
394
- end
395
-
396
- @customization_plugin
397
- end
398
-
399
- # Retrieves a CustomizationSpecItem that matches the supplied name
400
- # @param vim [Connection] VI Connection to use
401
- # @param name [String] name of customization
402
- # @return [RbVmomi::VIM::CustomizationSpecItem]
403
- def find_customization(name)
404
- csm = config[:vim].serviceContent.customizationSpecManager
405
- csm.GetCustomizationSpec(:name => name)
406
- end
407
-
408
- # Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
409
- # @param ip [String] Any static IP address to use, otherwise DHCP
410
- # @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
411
- # @return [RbVmomi::VIM::CustomizationIPSettings]
412
- def generate_adapter_map (ip=nil, gw=nil, dns1=nil, dns2=nil, domain=nil)
413
-
414
- settings = RbVmomi::VIM.CustomizationIPSettings
415
-
416
- if ip.nil?
417
- settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator
418
- else
419
- cidr_ip = NetAddr::CIDR.create(ip)
420
- settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cidr_ip.ip)
421
- settings.subnetMask = cidr_ip.netmask_ext
422
-
423
- # TODO - want to confirm gw/ip are in same subnet?
424
- # Only set gateway on first IP.
425
- if config[:customization_ips].split(',').first == ip
426
- if gw.nil?
427
- settings.gateway = [cidr_ip.network(:Objectify => true).next_ip]
428
- else
429
- gw_cidr = NetAddr::CIDR.create(gw)
430
- settings.gateway = [gw_cidr.ip]
431
- end
432
- end
433
- end
434
-
435
- adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
436
- adapter_map.adapter = settings
437
- adapter_map
438
- end
439
-
440
- def bootstrap_for_node()
441
- Chef::Knife::Bootstrap.load_deps
442
- bootstrap = Chef::Knife::Bootstrap.new
443
- bootstrap.name_args = [config[:fqdn]]
444
- bootstrap.config[:run_list] = get_config(:run_list).split(/[\s,]+/)
445
- bootstrap.config[:secret_file] = get_config(:secret_file)
446
- bootstrap.config[:ssh_user] = get_config(:ssh_user)
447
- bootstrap.config[:ssh_password] = get_config(:ssh_password)
448
- bootstrap.config[:ssh_port] = get_config(:ssh_port)
449
- bootstrap.config[:identity_file] = get_config(:identity_file)
450
- bootstrap.config[:chef_node_name] = get_config(:chef_node_name)
451
- bootstrap.config[:prerelease] = get_config(:prerelease)
452
- bootstrap.config[:bootstrap_version] = get_config(:bootstrap_version)
453
- bootstrap.config[:distro] = get_config(:distro)
454
- bootstrap.config[:use_sudo] = true unless get_config(:ssh_user) == 'root'
455
- bootstrap.config[:template_file] = get_config(:template_file)
456
- bootstrap.config[:environment] = get_config(:environment)
457
- bootstrap.config[:first_boot_attributes] = get_config(:first_boot_attributes)
458
- bootstrap.config[:log_level] = get_config(:log_level)
459
- # may be needed for vpc_mode
460
- bootstrap.config[:no_host_key_verify] = get_config(:no_host_key_verify)
461
- bootstrap
462
- end
463
-
464
- def tcp_test_ssh(hostname)
465
- tcp_socket = TCPSocket.new(hostname, get_config(:ssh_port))
466
- readable = IO.select([tcp_socket], nil, nil, 5)
467
- if readable
468
- Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
469
- true
470
- else
471
- false
472
- end
473
- rescue Errno::ETIMEDOUT
474
- false
475
- rescue Errno::EPERM
476
- false
477
- rescue Errno::ECONNREFUSED
478
- sleep 2
479
- false
480
- rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
481
- sleep 2
482
- false
483
- ensure
484
- tcp_socket && tcp_socket.close
485
- end
486
- end
1
+ #
2
+ # Author:: Ezra Pagel (<ezra@cpan.org>)
3
+ # Contributor:: Jesse Campbell (<hikeit@gmail.com>)
4
+ # Contributor:: Bethany Erskine (<bethany@paperlesspost.com>)
5
+ # Contributor:: Adrian Stanila (https://github.com/sacx)
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+
9
+ require 'chef/knife'
10
+ require 'chef/knife/base_vsphere_command'
11
+ require 'rbvmomi'
12
+ require 'netaddr'
13
+
14
+ # Clone an existing template into a new VM, optionally applying a customization specification.
15
+ # usage:
16
+ # knife vsphere vm clone NewNode UbuntuTemplate --cspec StaticSpec \
17
+ # --cips 192.168.0.99/24,192.168.1.99/24 \
18
+ # --chostname NODENAME --cdomain NODEDOMAIN
19
+ class Chef::Knife::VsphereVmClone < Chef::Knife::BaseVsphereCommand
20
+
21
+ banner "knife vsphere vm clone VMNAME (options)"
22
+
23
+ get_common_options
24
+
25
+ option :dest_folder,
26
+ :long => "--dest-folder FOLDER",
27
+ :description => "The folder into which to put the cloned VM"
28
+
29
+ option :datastore,
30
+ :long => "--datastore STORE",
31
+ :description => "The datastore into which to put the cloned VM"
32
+
33
+ option :resource_pool,
34
+ :long => "--resource-pool POOL",
35
+ :description => "The resource pool into which to put the cloned VM"
36
+
37
+ option :source_vm,
38
+ :long => "--template TEMPLATE",
39
+ :description => "The source VM / Template to clone from",
40
+ :required => true
41
+
42
+ option :annotation,
43
+ :long => "--annotation TEXT",
44
+ :description => "Add TEXT in Notes field from annotation"
45
+
46
+ option :customization_spec,
47
+ :long => "--cspec CUST_SPEC",
48
+ :description => "The name of any customization specification to apply"
49
+
50
+ option :customization_plugin,
51
+ :long => "--cplugin CUST_PLUGIN_PATH",
52
+ :description => "Path to plugin that implements KnifeVspherePlugin.customize_clone_spec and/or KnifeVspherePlugin.reconfig_vm"
53
+
54
+ option :customization_plugin_data,
55
+ :long => "--cplugin-data CUST_PLUGIN_DATA",
56
+ :description => "String of data to pass to the plugin. Use any format you wish."
57
+
58
+ option :customization_vlan,
59
+ :long => "--cvlan CUST_VLAN",
60
+ :description => "VLAN name for network adapter to join"
61
+
62
+ option :customization_ips,
63
+ :long => "--cips CUST_IPS",
64
+ :description => "Comma-delimited list of CIDR IPs for customization"
65
+
66
+ option :customization_dns_ips,
67
+ :long => "--cdnsips CUST_DNS_IPS",
68
+ :description => "Comma-delimited list of DNS IP addresses"
69
+
70
+ option :customization_dns_suffixes,
71
+ :long => "--cdnssuffix CUST_DNS_SUFFIXES",
72
+ :description => "Comma-delimited list of DNS search suffixes"
73
+
74
+ option :customization_gw,
75
+ :long => "--cgw CUST_GW",
76
+ :description => "CIDR IP of gateway for customization"
77
+
78
+ option :customization_hostname,
79
+ :long => "--chostname CUST_HOSTNAME",
80
+ :description => "Unqualified hostname for customization"
81
+
82
+ option :customization_domain,
83
+ :long => "--cdomain CUST_DOMAIN",
84
+ :description => "Domain name for customization"
85
+
86
+ option :customization_tz,
87
+ :long => "--ctz CUST_TIMEZONE",
88
+ :description => "Timezone invalid 'Area/Location' format"
89
+
90
+ option :customization_cpucount,
91
+ :long => "--ccpu CUST_CPU_COUNT",
92
+ :description => "Number of CPUs"
93
+
94
+ option :customization_memory,
95
+ :long => "--cram CUST_MEMORY_GB",
96
+ :description => "Gigabytes of RAM"
97
+
98
+ option :power,
99
+ :long => "--start",
100
+ :description => "Indicates whether to start the VM after a successful clone",
101
+ :boolean => false
102
+
103
+ option :bootstrap,
104
+ :long => "--bootstrap",
105
+ :description => "Indicates whether to bootstrap the VM",
106
+ :boolean => false
107
+
108
+ option :fqdn,
109
+ :long => "--fqdn SERVER_FQDN",
110
+ :description => "Fully qualified hostname for bootstrapping"
111
+
112
+ option :ssh_user,
113
+ :short => "-x USERNAME",
114
+ :long => "--ssh-user USERNAME",
115
+ :description => "The ssh username"
116
+ $default[:ssh_user] = "root"
117
+
118
+ option :ssh_password,
119
+ :short => "-P PASSWORD",
120
+ :long => "--ssh-password PASSWORD",
121
+ :description => "The ssh password"
122
+
123
+ option :ssh_port,
124
+ :short => "-p PORT",
125
+ :long => "--ssh-port PORT",
126
+ :description => "The ssh port"
127
+ $default[:ssh_port] = 22
128
+
129
+ option :identity_file,
130
+ :short => "-i IDENTITY_FILE",
131
+ :long => "--identity-file IDENTITY_FILE",
132
+ :description => "The SSH identity file used for authentication"
133
+
134
+ option :chef_node_name,
135
+ :short => "-N NAME",
136
+ :long => "--node-name NAME",
137
+ :description => "The Chef node name for your new node"
138
+
139
+ option :prerelease,
140
+ :long => "--prerelease",
141
+ :description => "Install the pre-release chef gems",
142
+ :boolean => false
143
+
144
+ option :bootstrap_version,
145
+ :long => "--bootstrap-version VERSION",
146
+ :description => "The version of Chef to install",
147
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
148
+
149
+ option :bootstrap_proxy,
150
+ :long => "--bootstrap-proxy PROXY_URL",
151
+ :description => "The proxy server for the node being bootstrapped",
152
+ :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
153
+
154
+ option :distro,
155
+ :short => "-d DISTRO",
156
+ :long => "--distro DISTRO",
157
+ :description => "Bootstrap a distro using a template"
158
+
159
+ option :template_file,
160
+ :long => "--template-file TEMPLATE",
161
+ :description => "Full path to location of template to use"
162
+
163
+ option :run_list,
164
+ :short => "-r RUN_LIST",
165
+ :long => "--run-list RUN_LIST",
166
+ :description => "Comma separated list of roles/recipes to apply"
167
+ $default[:run_list] = ''
168
+
169
+ option :secret_file,
170
+ :long => "--secret-file SECRET_FILE",
171
+ :description => "A file containing the secret key to use to encrypt data bag item values"
172
+ $default[:secret_file] = ''
173
+
174
+ option :no_host_key_verify,
175
+ :long => "--no-host-key-verify",
176
+ :description => "Disable host key verification",
177
+ :boolean => true
178
+
179
+ option :first_boot_attributes,
180
+ :short => "-j JSON_ATTRIBS",
181
+ :long => "--json-attributes",
182
+ :description => "A JSON string to be added to the first run of chef-client",
183
+ :proc => lambda { |o| JSON.parse(o) },
184
+ :default => {}
185
+
186
+ option :disable_customization,
187
+ :long => "--disable-customization",
188
+ :description => "Disable default customization",
189
+ :boolean => true,
190
+ :default => false
191
+
192
+ option :log_level,
193
+ :short => "-l LEVEL",
194
+ :long => "--log_level",
195
+ :description => "Set the log level (debug, info, warn, error, fatal) for chef-client",
196
+ :proc => lambda { |l| l.to_sym }
197
+
198
+ def run
199
+ $stdout.sync = true
200
+
201
+ vmname = @name_args[0]
202
+ if vmname.nil?
203
+ show_usage
204
+ fatal_exit("You must specify a virtual machine name")
205
+ end
206
+ config[:chef_node_name] = vmname unless config[:chef_node_name]
207
+ config[:vmname] = vmname
208
+
209
+ if get_config(:bootstrap) && get_config(:distro) && !@@chef_config_dir
210
+ fatal_exit("Can't find .chef for bootstrap files. chdir to a location with a .chef directory and try again")
211
+ end
212
+
213
+ vim = get_vim_connection
214
+
215
+ dc = get_datacenter
216
+
217
+ src_folder = find_folder(get_config(:folder)) || dc.vmFolder
218
+
219
+ src_vm = find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, config[:source_vm]) or
220
+ abort "VM/Template not found"
221
+
222
+ clone_spec = generate_clone_spec(src_vm.config)
223
+
224
+ cust_folder = config[:dest_folder] || get_config(:folder)
225
+
226
+ dest_folder = cust_folder.nil? ? src_vm.vmFolder : find_folder(cust_folder)
227
+
228
+ task = src_vm.CloneVM_Task(:folder => dest_folder, :name => vmname, :spec => clone_spec)
229
+ puts "Cloning template #{config[:source_vm]} to new VM #{vmname}"
230
+ task.wait_for_completion
231
+ puts "Finished creating virtual machine #{vmname}"
232
+
233
+ if customization_plugin && customization_plugin.respond_to?(:reconfig_vm)
234
+ target_vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or abort "VM could not be found in #{dest_folder}"
235
+ customization_plugin.reconfig_vm(target_vm)
236
+ end
237
+
238
+ if get_config(:power) || get_config(:bootstrap)
239
+ vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or
240
+ fatal_exit("VM #{vmname} not found")
241
+ vm.PowerOnVM_Task.wait_for_completion
242
+ puts "Powered on virtual machine #{vmname}"
243
+ end
244
+
245
+ if get_config(:bootstrap)
246
+ sleep 2 until vm.guest.ipAddress
247
+ config[:fqdn] = vm.guest.ipAddress unless config[:fqdn]
248
+ print "Waiting for sshd..."
249
+ print "." until tcp_test_ssh(config[:fqdn])
250
+ puts "done"
251
+
252
+ bootstrap_for_node.run
253
+ end
254
+ end
255
+
256
+ # Builds a CloneSpec
257
+ def generate_clone_spec (src_config)
258
+
259
+ rspec = nil
260
+ if get_config(:resource_pool)
261
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => find_pool(get_config(:resource_pool)))
262
+ else
263
+ dc = get_datacenter
264
+ hosts = find_all_in_folder(dc.hostFolder, RbVmomi::VIM::ComputeResource)
265
+ rp = hosts.first.resourcePool
266
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => rp)
267
+ end
268
+
269
+ if get_config(:datastore)
270
+ rspec.datastore = find_datastore(get_config(:datastore))
271
+ end
272
+
273
+ clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
274
+ :powerOn => false,
275
+ :template => false)
276
+
277
+ clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new)
278
+
279
+ if get_config(:annotation)
280
+ clone_spec.config.annotation = get_config(:annotation)
281
+ end
282
+
283
+ if get_config(:customization_cpucount)
284
+ clone_spec.config.numCPUs = get_config(:customization_cpucount)
285
+ end
286
+
287
+ if get_config(:customization_memory)
288
+ clone_spec.config.memoryMB = Integer(get_config(:customization_memory)) * 1024
289
+ end
290
+
291
+ if get_config(:customization_vlan)
292
+ network = find_network(get_config(:customization_vlan))
293
+ card = src_config.hardware.device.find { |d| d.deviceInfo.label == "Network adapter 1" } or
294
+ abort "Can't find source network card to customize"
295
+ begin
296
+ switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(:switchUuid => network.config.distributedVirtualSwitch.uuid, :portgroupKey => network.key)
297
+ card.backing.port = switch_port
298
+ rescue
299
+ # not connected to a distibuted switch?
300
+ card.backing.deviceName = network.name
301
+ end
302
+ dev_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit")
303
+ clone_spec.config.deviceChange.push dev_spec
304
+ end
305
+
306
+ if get_config(:customization_spec)
307
+ csi = find_customization(get_config(:customization_spec)) or
308
+ fatal_exit("failed to find customization specification named #{get_config(:customization_spec)}")
309
+
310
+ if csi.info.type != "Linux"
311
+ fatal_exit("Only Linux customization specifications are currently supported")
312
+ end
313
+ cust_spec = csi.spec
314
+ else
315
+ global_ipset = RbVmomi::VIM.CustomizationGlobalIPSettings
316
+ cust_spec = RbVmomi::VIM.CustomizationSpec(:globalIPSettings => global_ipset)
317
+ end
318
+
319
+ if get_config(:customization_dns_ips)
320
+ cust_spec.globalIPSettings.dnsServerList = get_config(:customization_dns_ips).split(',')
321
+ end
322
+
323
+ if get_config(:customization_dns_suffixes)
324
+ cust_spec.globalIPSettings.dnsSuffixList = get_config(:customization_dns_suffixes).split(',')
325
+ end
326
+
327
+ if config[:customization_ips]
328
+ if get_config(:customization_gw)
329
+ cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i, get_config(:customization_gw)) }
330
+ else
331
+ cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
332
+ end
333
+ end
334
+
335
+ unless get_config(:disable_customization)
336
+ use_ident = !config[:customization_hostname].nil? || !get_config(:customization_domain).nil? || cust_spec.identity.nil?
337
+
338
+
339
+ if use_ident
340
+ # TODO - verify that we're deploying a linux spec, at least warn
341
+ ident = RbVmomi::VIM.CustomizationLinuxPrep
342
+
343
+ ident.hostName = RbVmomi::VIM.CustomizationFixedName
344
+ if config[:customization_hostname]
345
+ ident.hostName.name = config[:customization_hostname]
346
+ else
347
+ ident.hostName.name = config[:vmname]
348
+ end
349
+
350
+ if get_config(:customization_domain)
351
+ ident.domain = get_config(:customization_domain)
352
+ else
353
+ ident.domain = ''
354
+ end
355
+
356
+ cust_spec.identity = ident
357
+ end
358
+
359
+ if customization_plugin && customization_plugin.respond_to?(:customize_clone_spec)
360
+ clone_spec = customization_plugin.customize_clone_spec(src_config, clone_spec)
361
+ end
362
+
363
+ clone_spec.customization = cust_spec
364
+ end
365
+ clone_spec
366
+ end
367
+
368
+ # Loads the customization plugin if one was specified
369
+ # @return [KnifeVspherePlugin] the loaded and initialized plugin or nil
370
+ def customization_plugin
371
+ if @customization_plugin.nil?
372
+ if cplugin_path = get_config(:customization_plugin)
373
+ if File.exists? cplugin_path
374
+ require cplugin_path
375
+ else
376
+ abort "Customization plugin could not be found at #{cplugin_path}"
377
+ end
378
+
379
+ if Object.const_defined? 'KnifeVspherePlugin'
380
+ @customization_plugin = Object.const_get('KnifeVspherePlugin').new
381
+ if cplugin_data = get_config(:customization_plugin_data)
382
+ if @customization_plugin.respond_to?(:data=)
383
+ @customization_plugin.data = cplugin_data
384
+ else
385
+ abort "Customization plugin has no :data= accessor to receive the --cplugin-data argument. Define both or neither."
386
+ end
387
+ end
388
+ else
389
+ abort "KnifeVspherePlugin class is not defined in #{cplugin_path}"
390
+ end
391
+ end
392
+ end
393
+
394
+ @customization_plugin
395
+ end
396
+
397
+ # Retrieves a CustomizationSpecItem that matches the supplied name
398
+ # @param vim [Connection] VI Connection to use
399
+ # @param name [String] name of customization
400
+ # @return [RbVmomi::VIM::CustomizationSpecItem]
401
+ def find_customization(name)
402
+ csm = config[:vim].serviceContent.customizationSpecManager
403
+ csm.GetCustomizationSpec(:name => name)
404
+ end
405
+
406
+ # Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
407
+ # @param ip [String] Any static IP address to use, otherwise DHCP
408
+ # @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
409
+ # @return [RbVmomi::VIM::CustomizationIPSettings]
410
+ def generate_adapter_map (ip=nil, gw=nil, dns1=nil, dns2=nil, domain=nil)
411
+
412
+ settings = RbVmomi::VIM.CustomizationIPSettings
413
+
414
+ if ip.nil?
415
+ settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator
416
+ else
417
+ cidr_ip = NetAddr::CIDR.create(ip)
418
+ settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cidr_ip.ip)
419
+ settings.subnetMask = cidr_ip.netmask_ext
420
+
421
+ # TODO - want to confirm gw/ip are in same subnet?
422
+ # Only set gateway on first IP.
423
+ if config[:customization_ips].split(',').first == ip
424
+ if gw.nil?
425
+ settings.gateway = [cidr_ip.network(:Objectify => true).next_ip]
426
+ else
427
+ gw_cidr = NetAddr::CIDR.create(gw)
428
+ settings.gateway = [gw_cidr.ip]
429
+ end
430
+ end
431
+ end
432
+
433
+ adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
434
+ adapter_map.adapter = settings
435
+ adapter_map
436
+ end
437
+
438
+ def bootstrap_for_node()
439
+ Chef::Knife::Bootstrap.load_deps
440
+ bootstrap = Chef::Knife::Bootstrap.new
441
+ bootstrap.name_args = [config[:fqdn]]
442
+ bootstrap.config[:run_list] = get_config(:run_list).split(/[\s,]+/)
443
+ bootstrap.config[:secret_file] = get_config(:secret_file)
444
+ bootstrap.config[:ssh_user] = get_config(:ssh_user)
445
+ bootstrap.config[:ssh_password] = get_config(:ssh_password)
446
+ bootstrap.config[:ssh_port] = get_config(:ssh_port)
447
+ bootstrap.config[:identity_file] = get_config(:identity_file)
448
+ bootstrap.config[:chef_node_name] = get_config(:chef_node_name)
449
+ bootstrap.config[:prerelease] = get_config(:prerelease)
450
+ bootstrap.config[:bootstrap_version] = get_config(:bootstrap_version)
451
+ bootstrap.config[:distro] = get_config(:distro)
452
+ bootstrap.config[:use_sudo] = true unless get_config(:ssh_user) == 'root'
453
+ bootstrap.config[:template_file] = get_config(:template_file)
454
+ bootstrap.config[:environment] = get_config(:environment)
455
+ bootstrap.config[:first_boot_attributes] = get_config(:first_boot_attributes)
456
+ bootstrap.config[:log_level] = get_config(:log_level)
457
+ # may be needed for vpc_mode
458
+ bootstrap.config[:no_host_key_verify] = get_config(:no_host_key_verify)
459
+ bootstrap
460
+ end
461
+
462
+ def tcp_test_ssh(hostname)
463
+ tcp_socket = TCPSocket.new(hostname, get_config(:ssh_port))
464
+ readable = IO.select([tcp_socket], nil, nil, 5)
465
+ if readable
466
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
467
+ true
468
+ else
469
+ false
470
+ end
471
+ rescue Errno::ETIMEDOUT
472
+ false
473
+ rescue Errno::EPERM
474
+ false
475
+ rescue Errno::ECONNREFUSED
476
+ sleep 2
477
+ false
478
+ rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
479
+ sleep 2
480
+ false
481
+ ensure
482
+ tcp_socket && tcp_socket.close
483
+ end
484
+ end