knife-vsphere 1.0.0.pre.2 → 1.0.0.pre.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,37 +1,37 @@
1
- # Author: Jesse Campbell
2
- #
3
- # Permission to use, copy, modify, and/or distribute this software for
4
- # any purpose with or without fee is hereby granted, provided that the
5
- # above copyright notice and this permission notice appear in all
6
- # copies.
7
- #
8
- # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
9
- # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
10
- # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
11
- # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
12
- # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
13
- # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
- # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
- # PERFORMANCE OF THIS SOFTWARE
16
-
17
- require 'chef/knife'
18
- require 'chef/knife/base_vsphere_command'
19
-
20
- # Lists all known data stores in datacenter with sizes
21
- class Chef::Knife::VsphereVlanList < Chef::Knife::BaseVsphereCommand
22
-
23
- banner "knife vsphere vlan list"
24
-
25
- get_common_options
26
-
27
- def run
28
- $stdout.sync = true
29
-
30
- vim = get_vim_connection
31
- dc = get_datacenter
32
- dc.network.each do |network|
33
- puts "#{ui.color("VLAN", :cyan)}: #{network.name}"
34
- end
35
- end
36
- end
37
-
1
+ # Author: Jesse Campbell
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for
4
+ # any purpose with or without fee is hereby granted, provided that the
5
+ # above copyright notice and this permission notice appear in all
6
+ # copies.
7
+ #
8
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
9
+ # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
10
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
11
+ # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
12
+ # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
13
+ # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ # PERFORMANCE OF THIS SOFTWARE
16
+
17
+ require 'chef/knife'
18
+ require 'chef/knife/base_vsphere_command'
19
+
20
+ # Lists all known data stores in datacenter with sizes
21
+ class Chef::Knife::VsphereVlanList < Chef::Knife::BaseVsphereCommand
22
+
23
+ banner "knife vsphere vlan list"
24
+
25
+ get_common_options
26
+
27
+ def run
28
+ $stdout.sync = true
29
+
30
+ vim = get_vim_connection
31
+ dc = get_datacenter
32
+ dc.network.each do |network|
33
+ puts "#{ui.color("VLAN", :cyan)}: #{network.name}"
34
+ end
35
+ end
36
+ end
37
+
@@ -1,539 +1,567 @@
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 :datastorecluster,
34
- :long => "--datastorecluster STORE",
35
- :description => "The datastorecluster into which to put the cloned VM"
36
-
37
- option :resource_pool,
38
- :long => "--resource-pool POOL",
39
- :description => "The resource pool into which to put the cloned VM"
40
-
41
- option :source_vm,
42
- :long => "--template TEMPLATE",
43
- :description => "The source VM / Template to clone from",
44
- :required => true
45
-
46
- option :linked_clone,
47
- :long => "--linked-clone",
48
- :description => "Indicates whether to use linked clones.",
49
- :boolean => false
50
-
51
- option :annotation,
52
- :long => "--annotation TEXT",
53
- :description => "Add TEXT in Notes field from annotation"
54
-
55
- option :customization_spec,
56
- :long => "--cspec CUST_SPEC",
57
- :description => "The name of any customization specification to apply"
58
-
59
- option :customization_plugin,
60
- :long => "--cplugin CUST_PLUGIN_PATH",
61
- :description => "Path to plugin that implements KnifeVspherePlugin.customize_clone_spec and/or KnifeVspherePlugin.reconfig_vm"
62
-
63
- option :customization_plugin_data,
64
- :long => "--cplugin-data CUST_PLUGIN_DATA",
65
- :description => "String of data to pass to the plugin. Use any format you wish."
66
-
67
- option :customization_vlan,
68
- :long => "--cvlan CUST_VLAN",
69
- :description => "VLAN name for network adapter to join"
70
-
71
- option :customization_ips,
72
- :long => "--cips CUST_IPS",
73
- :description => "Comma-delimited list of CIDR IPs for customization"
74
-
75
- option :customization_dns_ips,
76
- :long => "--cdnsips CUST_DNS_IPS",
77
- :description => "Comma-delimited list of DNS IP addresses"
78
-
79
- option :customization_dns_suffixes,
80
- :long => "--cdnssuffix CUST_DNS_SUFFIXES",
81
- :description => "Comma-delimited list of DNS search suffixes"
82
-
83
- option :customization_gw,
84
- :long => "--cgw CUST_GW",
85
- :description => "CIDR IP of gateway for customization"
86
-
87
- option :customization_hostname,
88
- :long => "--chostname CUST_HOSTNAME",
89
- :description => "Unqualified hostname for customization"
90
-
91
- option :customization_domain,
92
- :long => "--cdomain CUST_DOMAIN",
93
- :description => "Domain name for customization"
94
-
95
- option :customization_tz,
96
- :long => "--ctz CUST_TIMEZONE",
97
- :description => "Timezone invalid 'Area/Location' format"
98
-
99
- option :customization_cpucount,
100
- :long => "--ccpu CUST_CPU_COUNT",
101
- :description => "Number of CPUs"
102
-
103
- option :customization_memory,
104
- :long => "--cram CUST_MEMORY_GB",
105
- :description => "Gigabytes of RAM"
106
-
107
- option :power,
108
- :long => "--start",
109
- :description => "Indicates whether to start the VM after a successful clone",
110
- :boolean => false
111
-
112
- option :bootstrap,
113
- :long => "--bootstrap",
114
- :description => "Indicates whether to bootstrap the VM",
115
- :boolean => false
116
-
117
- option :fqdn,
118
- :long => "--fqdn SERVER_FQDN",
119
- :description => "Fully qualified hostname for bootstrapping"
120
-
121
- option :ssh_user,
122
- :short => "-x USERNAME",
123
- :long => "--ssh-user USERNAME",
124
- :description => "The ssh username"
125
- $default[:ssh_user] = "root"
126
-
127
- option :ssh_password,
128
- :short => "-P PASSWORD",
129
- :long => "--ssh-password PASSWORD",
130
- :description => "The ssh password"
131
-
132
- option :ssh_port,
133
- :short => "-p PORT",
134
- :long => "--ssh-port PORT",
135
- :description => "The ssh port"
136
- $default[:ssh_port] = 22
137
-
138
- option :identity_file,
139
- :short => "-i IDENTITY_FILE",
140
- :long => "--identity-file IDENTITY_FILE",
141
- :description => "The SSH identity file used for authentication"
142
-
143
- option :chef_node_name,
144
- :short => "-N NAME",
145
- :long => "--node-name NAME",
146
- :description => "The Chef node name for your new node"
147
-
148
- option :prerelease,
149
- :long => "--prerelease",
150
- :description => "Install the pre-release chef gems",
151
- :boolean => false
152
-
153
- option :bootstrap_version,
154
- :long => "--bootstrap-version VERSION",
155
- :description => "The version of Chef to install",
156
- :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
157
-
158
- option :bootstrap_proxy,
159
- :long => "--bootstrap-proxy PROXY_URL",
160
- :description => "The proxy server for the node being bootstrapped",
161
- :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
162
-
163
- option :distro,
164
- :short => "-d DISTRO",
165
- :long => "--distro DISTRO",
166
- :description => "Bootstrap a distro using a template"
167
-
168
- option :template_file,
169
- :long => "--template-file TEMPLATE",
170
- :description => "Full path to location of template to use"
171
-
172
- option :run_list,
173
- :short => "-r RUN_LIST",
174
- :long => "--run-list RUN_LIST",
175
- :description => "Comma separated list of roles/recipes to apply"
176
- $default[:run_list] = ''
177
-
178
- option :secret_file,
179
- :long => "--secret-file SECRET_FILE",
180
- :description => "A file containing the secret key to use to encrypt data bag item values"
181
- $default[:secret_file] = ''
182
-
183
- option :no_host_key_verify,
184
- :long => "--no-host-key-verify",
185
- :description => "Disable host key verification",
186
- :boolean => true
187
-
188
- option :first_boot_attributes,
189
- :short => "-j JSON_ATTRIBS",
190
- :long => "--json-attributes",
191
- :description => "A JSON string to be added to the first run of chef-client",
192
- :proc => lambda { |o| JSON.parse(o) },
193
- :default => {}
194
-
195
- option :disable_customization,
196
- :long => "--disable-customization",
197
- :description => "Disable default customization",
198
- :boolean => true,
199
- :default => false
200
-
201
- option :log_level,
202
- :short => "-l LEVEL",
203
- :long => "--log_level",
204
- :description => "Set the log level (debug, info, warn, error, fatal) for chef-client",
205
- :proc => lambda { |l| l.to_sym }
206
-
207
- def run
208
- $stdout.sync = true
209
-
210
- vmname = @name_args[0]
211
- if vmname.nil?
212
- show_usage
213
- fatal_exit("You must specify a virtual machine name")
214
- end
215
- config[:chef_node_name] = vmname unless config[:chef_node_name]
216
- config[:vmname] = vmname
217
-
218
- if get_config(:bootstrap) && get_config(:distro) && !@@chef_config_dir
219
- fatal_exit("Can't find .chef for bootstrap files. chdir to a location with a .chef directory and try again")
220
- end
221
-
222
- vim = get_vim_connection
223
-
224
- dc = get_datacenter
225
-
226
- src_folder = find_folder(get_config(:folder)) || dc.vmFolder
227
-
228
- src_vm = find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, config[:source_vm]) or
229
- abort "VM/Template not found"
230
-
231
- if get_config(:linked_clone)
232
- create_delta_disk(src_vm)
233
- end
234
-
235
- clone_spec = generate_clone_spec(src_vm.config)
236
-
237
- cust_folder = config[:dest_folder] || get_config(:folder)
238
-
239
- dest_folder = cust_folder.nil? ? src_vm.vmFolder : find_folder(cust_folder)
240
-
241
- task = src_vm.CloneVM_Task(:folder => dest_folder, :name => vmname, :spec => clone_spec)
242
- puts "Cloning template #{config[:source_vm]} to new VM #{vmname}"
243
- task.wait_for_completion
244
- puts "Finished creating virtual machine #{vmname}"
245
-
246
- if customization_plugin && customization_plugin.respond_to?(:reconfig_vm)
247
- target_vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or abort "VM could not be found in #{dest_folder}"
248
- customization_plugin.reconfig_vm(target_vm)
249
- end
250
-
251
- if get_config(:power) || get_config(:bootstrap)
252
- vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or
253
- fatal_exit("VM #{vmname} not found")
254
- vm.PowerOnVM_Task.wait_for_completion
255
- puts "Powered on virtual machine #{vmname}"
256
- end
257
-
258
- if get_config(:bootstrap)
259
- sleep 2 until vm.guest.ipAddress
260
- config[:fqdn] = vm.guest.ipAddress unless config[:fqdn]
261
- print "Waiting for sshd..."
262
- print "." until tcp_test_ssh(config[:fqdn])
263
- puts "done"
264
-
265
- bootstrap_for_node.run
266
- end
267
- end
268
-
269
- def create_delta_disk(src_vm)
270
- disks = src_vm.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk)
271
- disks.select { |disk| disk.backing.parent == nil }.each do |disk|
272
- spec = {
273
- :deviceChange => [
274
- {
275
- :operation => :remove,
276
- :device => disk
277
- },
278
- {
279
- :operation => :add,
280
- :fileOperation => :create,
281
- :device => disk.dup.tap { |new_disk|
282
- new_disk.backing = new_disk.backing.dup
283
- new_disk.backing.fileName = "[#{disk.backing.datastore.name}]"
284
- new_disk.backing.parent = disk.backing
285
- },
286
- }
287
- ]
288
- }
289
- src_vm.ReconfigVM_Task(:spec => spec).wait_for_completion
290
- end
291
- end
292
-
293
- # Builds a CloneSpec
294
- def generate_clone_spec (src_config)
295
-
296
- rspec = nil
297
- if get_config(:resource_pool)
298
- rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => find_pool(get_config(:resource_pool)))
299
- else
300
- dc = get_datacenter
301
- hosts = find_all_in_folder(dc.hostFolder, RbVmomi::VIM::ComputeResource)
302
- rp = hosts.first.resourcePool
303
- rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => rp)
304
- end
305
-
306
- if get_config(:linked_clone)
307
- rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:diskMoveType => :moveChildMostDiskBacking)
308
- end
309
-
310
- if get_config(:datastore) && get_config(:datastorecluster)
311
- abort "Please select either datastore or datastorecluster"
312
- end
313
-
314
- if get_config(:datastore)
315
- rspec.datastore = find_datastore(get_config(:datastore))
316
- end
317
-
318
- if get_config(:datastorecluster)
319
- dsc = find_datastorecluster(get_config(:datastorecluster))
320
-
321
- dsc.childEntity.each do |store|
322
- if (rspec.datastore == nil or rspec.datastore.summary[:freeSpace] < store.summary[:freeSpace])
323
- rspec.datastore = store
324
- end
325
- end
326
- end
327
-
328
- clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
329
- :powerOn => false,
330
- :template => false)
331
-
332
- clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new)
333
-
334
- if get_config(:annotation)
335
- clone_spec.config.annotation = get_config(:annotation)
336
- end
337
-
338
- if get_config(:customization_cpucount)
339
- clone_spec.config.numCPUs = get_config(:customization_cpucount)
340
- end
341
-
342
- if get_config(:customization_memory)
343
- clone_spec.config.memoryMB = Integer(get_config(:customization_memory)) * 1024
344
- end
345
-
346
- if get_config(:customization_vlan)
347
- network = find_network(get_config(:customization_vlan))
348
- card = src_config.hardware.device.grep(RbVmomi::VIM::VirtualEthernetCard).first or
349
- abort "Can't find source network card to customize"
350
- begin
351
- switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(:switchUuid => network.config.distributedVirtualSwitch.uuid, :portgroupKey => network.key)
352
- card.backing.port = switch_port
353
- rescue
354
- # not connected to a distibuted switch?
355
- card.backing.deviceName = network.name
356
- end
357
- dev_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit")
358
- clone_spec.config.deviceChange.push dev_spec
359
- end
360
-
361
- if get_config(:customization_spec)
362
- csi = find_customization(get_config(:customization_spec)) or
363
- fatal_exit("failed to find customization specification named #{get_config(:customization_spec)}")
364
-
365
- if csi.info.type != "Linux"
366
- fatal_exit("Only Linux customization specifications are currently supported")
367
- end
368
- cust_spec = csi.spec
369
- else
370
- global_ipset = RbVmomi::VIM.CustomizationGlobalIPSettings
371
- cust_spec = RbVmomi::VIM.CustomizationSpec(:globalIPSettings => global_ipset)
372
- end
373
-
374
- if get_config(:customization_dns_ips)
375
- cust_spec.globalIPSettings.dnsServerList = get_config(:customization_dns_ips).split(',')
376
- end
377
-
378
- if get_config(:customization_dns_suffixes)
379
- cust_spec.globalIPSettings.dnsSuffixList = get_config(:customization_dns_suffixes).split(',')
380
- end
381
-
382
- if config[:customization_ips]
383
- if get_config(:customization_gw)
384
- cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i, get_config(:customization_gw)) }
385
- else
386
- cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
387
- end
388
- end
389
-
390
- unless get_config(:disable_customization)
391
- use_ident = !config[:customization_hostname].nil? || !get_config(:customization_domain).nil? || cust_spec.identity.nil?
392
-
393
-
394
- if use_ident
395
- # TODO - verify that we're deploying a linux spec, at least warn
396
- ident = RbVmomi::VIM.CustomizationLinuxPrep
397
-
398
- ident.hostName = RbVmomi::VIM.CustomizationFixedName
399
- if config[:customization_hostname]
400
- ident.hostName.name = config[:customization_hostname]
401
- else
402
- ident.hostName.name = config[:vmname]
403
- end
404
-
405
- if get_config(:customization_domain)
406
- ident.domain = get_config(:customization_domain)
407
- else
408
- ident.domain = ''
409
- end
410
-
411
- cust_spec.identity = ident
412
- end
413
-
414
- clone_spec.customization = cust_spec
415
-
416
- if customization_plugin && customization_plugin.respond_to?(:customize_clone_spec)
417
- clone_spec = customization_plugin.customize_clone_spec(src_config, clone_spec)
418
- end
419
- end
420
- clone_spec
421
- end
422
-
423
- # Loads the customization plugin if one was specified
424
- # @return [KnifeVspherePlugin] the loaded and initialized plugin or nil
425
- def customization_plugin
426
- if @customization_plugin.nil?
427
- if cplugin_path = get_config(:customization_plugin)
428
- if File.exists? cplugin_path
429
- require cplugin_path
430
- else
431
- abort "Customization plugin could not be found at #{cplugin_path}"
432
- end
433
-
434
- if Object.const_defined? 'KnifeVspherePlugin'
435
- @customization_plugin = Object.const_get('KnifeVspherePlugin').new
436
- if cplugin_data = get_config(:customization_plugin_data)
437
- if @customization_plugin.respond_to?(:data=)
438
- @customization_plugin.data = cplugin_data
439
- else
440
- abort "Customization plugin has no :data= accessor to receive the --cplugin-data argument. Define both or neither."
441
- end
442
- end
443
- else
444
- abort "KnifeVspherePlugin class is not defined in #{cplugin_path}"
445
- end
446
- end
447
- end
448
-
449
- @customization_plugin
450
- end
451
-
452
- # Retrieves a CustomizationSpecItem that matches the supplied name
453
- # @param vim [Connection] VI Connection to use
454
- # @param name [String] name of customization
455
- # @return [RbVmomi::VIM::CustomizationSpecItem]
456
- def find_customization(name)
457
- csm = config[:vim].serviceContent.customizationSpecManager
458
- csm.GetCustomizationSpec(:name => name)
459
- end
460
-
461
- # Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
462
- # @param ip [String] Any static IP address to use, otherwise DHCP
463
- # @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
464
- # @return [RbVmomi::VIM::CustomizationIPSettings]
465
- def generate_adapter_map (ip=nil, gw=nil, dns1=nil, dns2=nil, domain=nil)
466
-
467
- settings = RbVmomi::VIM.CustomizationIPSettings
468
-
469
- if ip.nil?
470
- settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator
471
- else
472
- cidr_ip = NetAddr::CIDR.create(ip)
473
- settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cidr_ip.ip)
474
- settings.subnetMask = cidr_ip.netmask_ext
475
-
476
- # TODO - want to confirm gw/ip are in same subnet?
477
- # Only set gateway on first IP.
478
- if config[:customization_ips].split(',').first == ip
479
- if gw.nil?
480
- settings.gateway = [cidr_ip.network(:Objectify => true).next_ip]
481
- else
482
- gw_cidr = NetAddr::CIDR.create(gw)
483
- settings.gateway = [gw_cidr.ip]
484
- end
485
- end
486
- end
487
-
488
- adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
489
- adapter_map.adapter = settings
490
- adapter_map
491
- end
492
-
493
- def bootstrap_for_node()
494
- Chef::Knife::Bootstrap.load_deps
495
- bootstrap = Chef::Knife::Bootstrap.new
496
- bootstrap.name_args = [config[:fqdn]]
497
- bootstrap.config[:run_list] = get_config(:run_list).split(/[\s,]+/)
498
- bootstrap.config[:secret_file] = get_config(:secret_file)
499
- bootstrap.config[:ssh_user] = get_config(:ssh_user)
500
- bootstrap.config[:ssh_password] = get_config(:ssh_password)
501
- bootstrap.config[:ssh_port] = get_config(:ssh_port)
502
- bootstrap.config[:identity_file] = get_config(:identity_file)
503
- bootstrap.config[:chef_node_name] = get_config(:chef_node_name)
504
- bootstrap.config[:prerelease] = get_config(:prerelease)
505
- bootstrap.config[:bootstrap_version] = get_config(:bootstrap_version)
506
- bootstrap.config[:distro] = get_config(:distro)
507
- bootstrap.config[:use_sudo] = true unless get_config(:ssh_user) == 'root'
508
- bootstrap.config[:template_file] = get_config(:template_file)
509
- bootstrap.config[:environment] = get_config(:environment)
510
- bootstrap.config[:first_boot_attributes] = get_config(:first_boot_attributes)
511
- bootstrap.config[:log_level] = get_config(:log_level)
512
- # may be needed for vpc_mode
513
- bootstrap.config[:no_host_key_verify] = get_config(:no_host_key_verify)
514
- bootstrap
515
- end
516
-
517
- def tcp_test_ssh(hostname)
518
- tcp_socket = TCPSocket.new(hostname, get_config(:ssh_port))
519
- readable = IO.select([tcp_socket], nil, nil, 5)
520
- if readable
521
- Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
522
- true
523
- else
524
- false
525
- end
526
- rescue Errno::ETIMEDOUT
527
- false
528
- rescue Errno::EPERM
529
- false
530
- rescue Errno::ECONNREFUSED
531
- sleep 2
532
- false
533
- rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
534
- sleep 2
535
- false
536
- ensure
537
- tcp_socket && tcp_socket.close
538
- end
539
- 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 :datastorecluster,
34
+ :long => "--datastorecluster STORE",
35
+ :description => "The datastorecluster into which to put the cloned VM"
36
+
37
+ option :resource_pool,
38
+ :long => "--resource-pool POOL",
39
+ :description => "The resource pool into which to put the cloned VM"
40
+
41
+ option :source_vm,
42
+ :long => "--template TEMPLATE",
43
+ :description => "The source VM / Template to clone from"
44
+
45
+ option :linked_clone,
46
+ :long => "--linked-clone",
47
+ :description => "Indicates whether to use linked clones.",
48
+ :boolean => false
49
+
50
+ option :annotation,
51
+ :long => "--annotation TEXT",
52
+ :description => "Add TEXT in Notes field from annotation"
53
+
54
+ option :customization_spec,
55
+ :long => "--cspec CUST_SPEC",
56
+ :description => "The name of any customization specification to apply"
57
+
58
+ option :customization_plugin,
59
+ :long => "--cplugin CUST_PLUGIN_PATH",
60
+ :description => "Path to plugin that implements KnifeVspherePlugin.customize_clone_spec and/or KnifeVspherePlugin.reconfig_vm"
61
+
62
+ option :customization_plugin_data,
63
+ :long => "--cplugin-data CUST_PLUGIN_DATA",
64
+ :description => "String of data to pass to the plugin. Use any format you wish."
65
+
66
+ option :customization_vlan,
67
+ :long => "--cvlan CUST_VLAN",
68
+ :description => "VLAN name for network adapter to join"
69
+
70
+ option :customization_ips,
71
+ :long => "--cips CUST_IPS",
72
+ :description => "Comma-delimited list of CIDR IPs for customization"
73
+
74
+ option :customization_dns_ips,
75
+ :long => "--cdnsips CUST_DNS_IPS",
76
+ :description => "Comma-delimited list of DNS IP addresses"
77
+
78
+ option :customization_dns_suffixes,
79
+ :long => "--cdnssuffix CUST_DNS_SUFFIXES",
80
+ :description => "Comma-delimited list of DNS search suffixes"
81
+
82
+ option :customization_gw,
83
+ :long => "--cgw CUST_GW",
84
+ :description => "CIDR IP of gateway for customization"
85
+
86
+ option :customization_hostname,
87
+ :long => "--chostname CUST_HOSTNAME",
88
+ :description => "Unqualified hostname for customization"
89
+
90
+ option :customization_domain,
91
+ :long => "--cdomain CUST_DOMAIN",
92
+ :description => "Domain name for customization"
93
+
94
+ option :customization_tz,
95
+ :long => "--ctz CUST_TIMEZONE",
96
+ :description => "Timezone invalid 'Area/Location' format"
97
+
98
+ option :customization_cpucount,
99
+ :long => "--ccpu CUST_CPU_COUNT",
100
+ :description => "Number of CPUs"
101
+
102
+ option :customization_memory,
103
+ :long => "--cram CUST_MEMORY_GB",
104
+ :description => "Gigabytes of RAM"
105
+
106
+ option :power,
107
+ :long => "--start",
108
+ :description => "Indicates whether to start the VM after a successful clone",
109
+ :boolean => false
110
+
111
+ option :bootstrap,
112
+ :long => "--bootstrap",
113
+ :description => "Indicates whether to bootstrap the VM",
114
+ :boolean => false
115
+
116
+ option :fqdn,
117
+ :long => "--fqdn SERVER_FQDN",
118
+ :description => "Fully qualified hostname for bootstrapping"
119
+
120
+ option :ssh_user,
121
+ :short => "-x USERNAME",
122
+ :long => "--ssh-user USERNAME",
123
+ :description => "The ssh username"
124
+ $default[:ssh_user] = "root"
125
+
126
+ option :ssh_password,
127
+ :short => "-P PASSWORD",
128
+ :long => "--ssh-password PASSWORD",
129
+ :description => "The ssh password"
130
+
131
+ option :ssh_port,
132
+ :short => "-p PORT",
133
+ :long => "--ssh-port PORT",
134
+ :description => "The ssh port"
135
+ $default[:ssh_port] = 22
136
+
137
+ option :identity_file,
138
+ :short => "-i IDENTITY_FILE",
139
+ :long => "--identity-file IDENTITY_FILE",
140
+ :description => "The SSH identity file used for authentication"
141
+
142
+ option :chef_node_name,
143
+ :short => "-N NAME",
144
+ :long => "--node-name NAME",
145
+ :description => "The Chef node name for your new node"
146
+
147
+ option :prerelease,
148
+ :long => "--prerelease",
149
+ :description => "Install the pre-release chef gems",
150
+ :boolean => false
151
+
152
+ option :bootstrap_version,
153
+ :long => "--bootstrap-version VERSION",
154
+ :description => "The version of Chef to install",
155
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
156
+
157
+ option :bootstrap_proxy,
158
+ :long => "--bootstrap-proxy PROXY_URL",
159
+ :description => "The proxy server for the node being bootstrapped",
160
+ :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
161
+
162
+ option :distro,
163
+ :short => "-d DISTRO",
164
+ :long => "--distro DISTRO",
165
+ :description => "Bootstrap a distro using a template"
166
+
167
+ option :template_file,
168
+ :long => "--template-file TEMPLATE",
169
+ :description => "Full path to location of template to use"
170
+
171
+ option :run_list,
172
+ :short => "-r RUN_LIST",
173
+ :long => "--run-list RUN_LIST",
174
+ :description => "Comma separated list of roles/recipes to apply"
175
+ $default[:run_list] = ''
176
+
177
+ option :secret_file,
178
+ :long => "--secret-file SECRET_FILE",
179
+ :description => "A file containing the secret key to use to encrypt data bag item values"
180
+ $default[:secret_file] = ''
181
+
182
+ option :hint,
183
+ :long => "--hint HINT_NAME[=HINT_FILE]",
184
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
185
+ :proc => Proc.new { |h|
186
+ Chef::Config[:knife][:hints] ||= Hash.new
187
+ name, path = h.split("=")
188
+ Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new }
189
+ $default[:hint] = ''
190
+
191
+ option :no_host_key_verify,
192
+ :long => "--no-host-key-verify",
193
+ :description => "Disable host key verification",
194
+ :boolean => true
195
+
196
+ option :first_boot_attributes,
197
+ :short => "-j JSON_ATTRIBS",
198
+ :long => "--json-attributes",
199
+ :description => "A JSON string to be added to the first run of chef-client",
200
+ :proc => lambda { |o| JSON.parse(o) },
201
+ :default => {}
202
+
203
+ option :disable_customization,
204
+ :long => "--disable-customization",
205
+ :description => "Disable default customization",
206
+ :boolean => true,
207
+ :default => false
208
+
209
+ option :log_level,
210
+ :short => "-l LEVEL",
211
+ :long => "--log_level",
212
+ :description => "Set the log level (debug, info, warn, error, fatal) for chef-client",
213
+ :proc => lambda { |l| l.to_sym }
214
+
215
+ option :mark_as_template,
216
+ :long => "--mark_as_template",
217
+ :description => "Indicates whether to mark the new vm as a template",
218
+ :boolean => false
219
+
220
+ def run
221
+ $stdout.sync = true
222
+
223
+ vmname = @name_args[0]
224
+ if vmname.nil?
225
+ show_usage
226
+ fatal_exit("You must specify a virtual machine name")
227
+ end
228
+ config[:chef_node_name] = vmname unless config[:chef_node_name]
229
+ config[:vmname] = vmname
230
+
231
+ if get_config(:bootstrap) && get_config(:distro) && !@@chef_config_dir
232
+ fatal_exit("Can't find .chef for bootstrap files. chdir to a location with a .chef directory and try again")
233
+ end
234
+
235
+ vim = get_vim_connection
236
+
237
+ dc = get_datacenter
238
+
239
+ src_folder = find_folder(get_config(:folder)) || dc.vmFolder
240
+
241
+ abort "--template or knife[:source_vm] must be specified" unless config[:source_vm]
242
+
243
+ src_vm = find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, config[:source_vm]) or
244
+ abort "VM/Template not found"
245
+
246
+ if get_config(:linked_clone)
247
+ create_delta_disk(src_vm)
248
+ end
249
+
250
+ clone_spec = generate_clone_spec(src_vm.config)
251
+
252
+ cust_folder = config[:dest_folder] || get_config(:folder)
253
+
254
+ dest_folder = cust_folder.nil? ? src_vm.vmFolder : find_folder(cust_folder)
255
+
256
+ task = src_vm.CloneVM_Task(:folder => dest_folder, :name => vmname, :spec => clone_spec)
257
+ puts "Cloning template #{config[:source_vm]} to new VM #{vmname}"
258
+ task.wait_for_completion
259
+ puts "Finished creating virtual machine #{vmname}"
260
+
261
+ if customization_plugin && customization_plugin.respond_to?(:reconfig_vm)
262
+ target_vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or abort "VM could not be found in #{dest_folder}"
263
+ customization_plugin.reconfig_vm(target_vm)
264
+ end
265
+
266
+ if !get_config(:mark_as_template)
267
+ if get_config(:power) || get_config(:bootstrap)
268
+ vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or
269
+ fatal_exit("VM #{vmname} not found")
270
+ vm.PowerOnVM_Task.wait_for_completion
271
+ puts "Powered on virtual machine #{vmname}"
272
+ end
273
+
274
+
275
+ if get_config(:bootstrap)
276
+ sleep 2 until vm.guest.ipAddress
277
+ config[:fqdn] = vm.guest.ipAddress unless config[:fqdn]
278
+ print "Waiting for sshd..."
279
+ print "." until tcp_test_ssh(config[:fqdn])
280
+ puts "done"
281
+
282
+ bootstrap_for_node.run
283
+ end
284
+ end
285
+ end
286
+
287
+ def create_delta_disk(src_vm)
288
+ disks = src_vm.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk)
289
+ disks.select { |disk| disk.backing.parent == nil }.each do |disk|
290
+ spec = {
291
+ :deviceChange => [
292
+ {
293
+ :operation => :remove,
294
+ :device => disk
295
+ },
296
+ {
297
+ :operation => :add,
298
+ :fileOperation => :create,
299
+ :device => disk.dup.tap { |new_disk|
300
+ new_disk.backing = new_disk.backing.dup
301
+ new_disk.backing.fileName = "[#{disk.backing.datastore.name}]"
302
+ new_disk.backing.parent = disk.backing
303
+ },
304
+ }
305
+ ]
306
+ }
307
+ src_vm.ReconfigVM_Task(:spec => spec).wait_for_completion
308
+ end
309
+ end
310
+
311
+ # Builds a CloneSpec
312
+ def generate_clone_spec (src_config)
313
+
314
+ rspec = nil
315
+ if get_config(:resource_pool)
316
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => find_pool(get_config(:resource_pool)))
317
+ else
318
+ dc = get_datacenter
319
+ hosts = find_all_in_folder(dc.hostFolder, RbVmomi::VIM::ComputeResource)
320
+ rp = hosts.first.resourcePool
321
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => rp)
322
+ end
323
+
324
+ if get_config(:linked_clone)
325
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:diskMoveType => :moveChildMostDiskBacking)
326
+ end
327
+
328
+ if get_config(:datastore) && get_config(:datastorecluster)
329
+ abort "Please select either datastore or datastorecluster"
330
+ end
331
+
332
+ if get_config(:datastore)
333
+ rspec.datastore = find_datastore(get_config(:datastore))
334
+ end
335
+
336
+ if get_config(:datastorecluster)
337
+ dsc = find_datastorecluster(get_config(:datastorecluster))
338
+
339
+ dsc.childEntity.each do |store|
340
+ if (rspec.datastore == nil or rspec.datastore.summary[:freeSpace] < store.summary[:freeSpace])
341
+ rspec.datastore = store
342
+ end
343
+ end
344
+ end
345
+
346
+ if get_config(:mark_as_template)
347
+ clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
348
+ :powerOn => false,
349
+ :template => true)
350
+ else
351
+ clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
352
+ :powerOn => false,
353
+ :template => false)
354
+ end
355
+ clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new)
356
+
357
+ if get_config(:annotation)
358
+ clone_spec.config.annotation = get_config(:annotation)
359
+ end
360
+
361
+ if get_config(:customization_cpucount)
362
+ clone_spec.config.numCPUs = get_config(:customization_cpucount)
363
+ end
364
+
365
+ if get_config(:customization_memory)
366
+ clone_spec.config.memoryMB = Integer(get_config(:customization_memory)) * 1024
367
+ end
368
+
369
+ if get_config(:customization_vlan)
370
+ network = find_network(get_config(:customization_vlan))
371
+ card = src_config.hardware.device.grep(RbVmomi::VIM::VirtualEthernetCard).first or
372
+ abort "Can't find source network card to customize"
373
+ begin
374
+ switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(:switchUuid => network.config.distributedVirtualSwitch.uuid, :portgroupKey => network.key)
375
+ card.backing.port = switch_port
376
+ rescue
377
+ # not connected to a distibuted switch?
378
+ card.backing.deviceName = network.name
379
+ end
380
+ dev_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit")
381
+ clone_spec.config.deviceChange.push dev_spec
382
+ end
383
+
384
+ if get_config(:customization_spec)
385
+ csi = find_customization(get_config(:customization_spec)) or
386
+ fatal_exit("failed to find customization specification named #{get_config(:customization_spec)}")
387
+
388
+ cust_spec = csi.spec
389
+ else
390
+ global_ipset = RbVmomi::VIM.CustomizationGlobalIPSettings
391
+ cust_spec = RbVmomi::VIM.CustomizationSpec(:globalIPSettings => global_ipset)
392
+ end
393
+
394
+ if get_config(:customization_dns_ips)
395
+ cust_spec.globalIPSettings.dnsServerList = get_config(:customization_dns_ips).split(',')
396
+ end
397
+
398
+ if get_config(:customization_dns_suffixes)
399
+ cust_spec.globalIPSettings.dnsSuffixList = get_config(:customization_dns_suffixes).split(',')
400
+ end
401
+
402
+ if config[:customization_ips]
403
+ if get_config(:customization_gw)
404
+ cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i, get_config(:customization_gw)) }
405
+ else
406
+ cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
407
+ end
408
+ end
409
+
410
+ unless get_config(:disable_customization)
411
+ use_ident = !config[:customization_hostname].nil? || !get_config(:customization_domain).nil? || cust_spec.identity.nil?
412
+
413
+ if use_ident
414
+ hostname = if config[:customization_hostname]
415
+ config[:customization_hostname]
416
+ else
417
+ config[:vmname]
418
+ end
419
+
420
+ if src_config.guestId.downcase.include?("linux")
421
+ ident = RbVmomi::VIM.CustomizationLinuxPrep
422
+
423
+ ident.hostName = RbVmomi::VIM.CustomizationFixedName(:name => hostname)
424
+
425
+ if get_config(:customization_domain)
426
+ ident.domain = get_config(:customization_domain)
427
+ else
428
+ ident.domain = ''
429
+ end
430
+
431
+ cust_spec.identity = ident
432
+ elsif src_config.guestId.downcase.include?("windows")
433
+ if cust_spec.identity.nil?
434
+ fatal_exit("Please provide Windows Guest Customization")
435
+ else
436
+ cust_spec.identity.userData.computerName = RbVmomi::VIM.CustomizationFixedName(:name => hostname)
437
+ end
438
+ end
439
+ end
440
+
441
+ clone_spec.customization = cust_spec
442
+
443
+ if customization_plugin && customization_plugin.respond_to?(:customize_clone_spec)
444
+ clone_spec = customization_plugin.customize_clone_spec(src_config, clone_spec)
445
+ end
446
+ end
447
+ clone_spec
448
+ end
449
+
450
+ # Loads the customization plugin if one was specified
451
+ # @return [KnifeVspherePlugin] the loaded and initialized plugin or nil
452
+ def customization_plugin
453
+ if @customization_plugin.nil?
454
+ if cplugin_path = get_config(:customization_plugin)
455
+ if File.exists? cplugin_path
456
+ require cplugin_path
457
+ else
458
+ abort "Customization plugin could not be found at #{cplugin_path}"
459
+ end
460
+
461
+ if Object.const_defined? 'KnifeVspherePlugin'
462
+ @customization_plugin = Object.const_get('KnifeVspherePlugin').new
463
+ if cplugin_data = get_config(:customization_plugin_data)
464
+ if @customization_plugin.respond_to?(:data=)
465
+ @customization_plugin.data = cplugin_data
466
+ else
467
+ abort "Customization plugin has no :data= accessor to receive the --cplugin-data argument. Define both or neither."
468
+ end
469
+ end
470
+ else
471
+ abort "KnifeVspherePlugin class is not defined in #{cplugin_path}"
472
+ end
473
+ end
474
+ end
475
+
476
+ @customization_plugin
477
+ end
478
+
479
+ # Retrieves a CustomizationSpecItem that matches the supplied name
480
+ # @param vim [Connection] VI Connection to use
481
+ # @param name [String] name of customization
482
+ # @return [RbVmomi::VIM::CustomizationSpecItem]
483
+ def find_customization(name)
484
+ csm = config[:vim].serviceContent.customizationSpecManager
485
+ csm.GetCustomizationSpec(:name => name)
486
+ end
487
+
488
+ # Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
489
+ # @param ip [String] Any static IP address to use, otherwise DHCP
490
+ # @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
491
+ # @return [RbVmomi::VIM::CustomizationIPSettings]
492
+ def generate_adapter_map (ip=nil, gw=nil, dns1=nil, dns2=nil, domain=nil)
493
+
494
+ settings = RbVmomi::VIM.CustomizationIPSettings
495
+
496
+ if ip.nil?
497
+ settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator
498
+ else
499
+ cidr_ip = NetAddr::CIDR.create(ip)
500
+ settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cidr_ip.ip)
501
+ settings.subnetMask = cidr_ip.netmask_ext
502
+
503
+ # TODO - want to confirm gw/ip are in same subnet?
504
+ # Only set gateway on first IP.
505
+ if config[:customization_ips].split(',').first == ip
506
+ if gw.nil?
507
+ settings.gateway = [cidr_ip.network(:Objectify => true).next_ip]
508
+ else
509
+ gw_cidr = NetAddr::CIDR.create(gw)
510
+ settings.gateway = [gw_cidr.ip]
511
+ end
512
+ end
513
+ end
514
+
515
+ adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
516
+ adapter_map.adapter = settings
517
+ adapter_map
518
+ end
519
+
520
+ def bootstrap_for_node()
521
+ Chef::Knife::Bootstrap.load_deps
522
+ bootstrap = Chef::Knife::Bootstrap.new
523
+ bootstrap.name_args = [config[:fqdn]]
524
+ bootstrap.config[:run_list] = get_config(:run_list).split(/[\s,]+/)
525
+ bootstrap.config[:secret_file] = get_config(:secret_file)
526
+ bootstrap.config[:hint] = get_config(:hint)
527
+ bootstrap.config[:ssh_user] = get_config(:ssh_user)
528
+ bootstrap.config[:ssh_password] = get_config(:ssh_password)
529
+ bootstrap.config[:ssh_port] = get_config(:ssh_port)
530
+ bootstrap.config[:identity_file] = get_config(:identity_file)
531
+ bootstrap.config[:chef_node_name] = get_config(:chef_node_name)
532
+ bootstrap.config[:prerelease] = get_config(:prerelease)
533
+ bootstrap.config[:bootstrap_version] = get_config(:bootstrap_version)
534
+ bootstrap.config[:distro] = get_config(:distro)
535
+ bootstrap.config[:use_sudo] = true unless get_config(:ssh_user) == 'root'
536
+ bootstrap.config[:template_file] = get_config(:template_file)
537
+ bootstrap.config[:environment] = get_config(:environment)
538
+ bootstrap.config[:first_boot_attributes] = get_config(:first_boot_attributes)
539
+ bootstrap.config[:log_level] = get_config(:log_level)
540
+ # may be needed for vpc_mode
541
+ bootstrap.config[:no_host_key_verify] = get_config(:no_host_key_verify)
542
+ bootstrap
543
+ end
544
+
545
+ def tcp_test_ssh(hostname)
546
+ tcp_socket = TCPSocket.new(hostname, get_config(:ssh_port))
547
+ readable = IO.select([tcp_socket], nil, nil, 5)
548
+ if readable
549
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
550
+ true
551
+ else
552
+ false
553
+ end
554
+ rescue Errno::ETIMEDOUT
555
+ false
556
+ rescue Errno::EPERM
557
+ false
558
+ rescue Errno::ECONNREFUSED
559
+ sleep 2
560
+ false
561
+ rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
562
+ sleep 2
563
+ false
564
+ ensure
565
+ tcp_socket && tcp_socket.close
566
+ end
567
+ end