knife-vsphere 0.9.5 → 0.9.6

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,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