knife-vsphere 1.0.1 → 1.2.0
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.
- checksums.yaml +4 -4
- data/lib/chef/knife/base_vsphere_command.rb +383 -371
- data/lib/chef/knife/customization_helper.rb +40 -0
- data/lib/chef/knife/vsphere_cluster_list.rb +47 -0
- data/lib/chef/knife/vsphere_cpu_ratio.rb +41 -45
- data/lib/chef/knife/vsphere_customization_list.rb +24 -29
- data/lib/chef/knife/vsphere_datastore_list.rb +68 -72
- data/lib/chef/knife/vsphere_datastore_maxfree.rb +48 -49
- data/lib/chef/knife/vsphere_datastorecluster_list.rb +66 -71
- data/lib/chef/knife/vsphere_datastorecluster_maxfree.rb +75 -84
- data/lib/chef/knife/vsphere_folder_list.rb +28 -30
- data/lib/chef/knife/vsphere_hosts_list.rb +42 -42
- data/lib/chef/knife/vsphere_pool_list.rb +46 -48
- data/lib/chef/knife/vsphere_pool_query.rb +58 -58
- data/lib/chef/knife/vsphere_template_list.rb +30 -32
- data/lib/chef/knife/vsphere_vlan_create.rb +51 -0
- data/lib/chef/knife/vsphere_vlan_list.rb +35 -37
- data/lib/chef/knife/vsphere_vm_clone.rb +834 -581
- data/lib/chef/knife/vsphere_vm_config.rb +48 -46
- data/lib/chef/knife/vsphere_vm_delete.rb +70 -66
- data/lib/chef/knife/vsphere_vm_execute.rb +62 -66
- data/lib/chef/knife/vsphere_vm_list.rb +57 -61
- data/lib/chef/knife/vsphere_vm_markastemplate.rb +48 -54
- data/lib/chef/knife/vsphere_vm_migrate.rb +73 -0
- data/lib/chef/knife/vsphere_vm_move.rb +88 -0
- data/lib/chef/knife/vsphere_vm_net.rb +57 -0
- data/lib/chef/knife/vsphere_vm_property_get.rb +44 -46
- data/lib/chef/knife/vsphere_vm_property_set.rb +83 -84
- data/lib/chef/knife/vsphere_vm_query.rb +48 -48
- data/lib/chef/knife/vsphere_vm_snapshot.rb +124 -130
- data/lib/chef/knife/vsphere_vm_state.rb +122 -127
- data/lib/chef/knife/vsphere_vm_toolsconfig.rb +54 -52
- data/lib/chef/knife/vsphere_vm_vmdk_add.rb +234 -241
- data/lib/chef/knife/vsphere_vm_wait_sysprep.rb +54 -0
- data/lib/knife-vsphere/version.rb +3 -4
- metadata +43 -15
- data/lib/chef/knife/vshpere_vm_migrate.rb +0 -80
- data/lib/chef/knife/vshpere_vm_move.rb +0 -92
- data/lib/chef/knife/vshpere_vm_net.rb +0 -57
@@ -1,58 +1,58 @@
|
|
1
|
-
require 'chef/knife'
|
2
|
-
require 'chef/knife/base_vsphere_command'
|
3
|
-
require 'rbvmomi'
|
4
|
-
require 'netaddr'
|
5
|
-
|
6
|
-
class Chef::Knife::VspherePoolQuery < Chef::Knife::BaseVsphereCommand
|
7
|
-
banner "knife vsphere pool query POOLNAME QUERY. See \"http://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.ComputeResource.html\" for allowed QUERY values."
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def traverse_folders_for_pool(folder, poolname)
|
12
|
-
children = folder.children.find_all
|
13
|
-
children.each do |child|
|
14
|
-
if child.class == RbVmomi::VIM::ClusterComputeResource || child.class == RbVmomi::VIM::ComputeResource || child.class == RbVmomi::VIM::ResourcePool
|
15
|
-
if child.name == poolname
|
16
|
-
elsif child.class == RbVmomi::VIM::Folder
|
17
|
-
pool = traverse_folders_for_pool(child, poolname)
|
18
|
-
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def run
|
25
|
-
$stdout.sync = true
|
26
|
-
poolname = @name_args[0]
|
27
|
-
if poolname.nil?
|
28
|
-
show_usage
|
29
|
-
fatal_exit(
|
30
|
-
end
|
31
|
-
|
32
|
-
query_string = @name_args[1]
|
33
|
-
if query_string.nil?
|
34
|
-
show_usage
|
35
|
-
fatal_exit(
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
dc =
|
41
|
-
folder = dc.hostFolder
|
42
|
-
|
43
|
-
pool = traverse_folders_for_pool(folder, poolname)
|
44
|
-
|
45
|
-
# split QUERY by dots, and walk the object model
|
46
|
-
query = query_string.split '.'
|
47
|
-
result = pool
|
48
|
-
query.each do |part|
|
49
|
-
message, index = part.split(/[\[\]]/)
|
50
|
-
unless result.respond_to? message.to_sym
|
51
|
-
fatal_exit("\"#{query_string}\" not recognized.")
|
52
|
-
end
|
53
|
-
|
54
|
-
result = index ? result.send(message)[index.to_i] : result.send(message)
|
55
|
-
end
|
56
|
-
puts result
|
57
|
-
end
|
58
|
-
end
|
1
|
+
require 'chef/knife'
|
2
|
+
require 'chef/knife/base_vsphere_command'
|
3
|
+
require 'rbvmomi'
|
4
|
+
require 'netaddr'
|
5
|
+
|
6
|
+
class Chef::Knife::VspherePoolQuery < Chef::Knife::BaseVsphereCommand
|
7
|
+
banner "knife vsphere pool query POOLNAME QUERY. See \"http://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.ComputeResource.html\" for allowed QUERY values."
|
8
|
+
|
9
|
+
common_options
|
10
|
+
|
11
|
+
def traverse_folders_for_pool(folder, poolname)
|
12
|
+
children = folder.children.find_all
|
13
|
+
children.each do |child|
|
14
|
+
if child.class == RbVmomi::VIM::ClusterComputeResource || child.class == RbVmomi::VIM::ComputeResource || child.class == RbVmomi::VIM::ResourcePool
|
15
|
+
return child if child.name == poolname
|
16
|
+
elsif child.class == RbVmomi::VIM::Folder
|
17
|
+
pool = traverse_folders_for_pool(child, poolname)
|
18
|
+
return pool if pool
|
19
|
+
end
|
20
|
+
end
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
$stdout.sync = true
|
26
|
+
poolname = @name_args[0]
|
27
|
+
if poolname.nil?
|
28
|
+
show_usage
|
29
|
+
fatal_exit('You must specify a resource poor or cluster name (see knife vsphere pool list)')
|
30
|
+
end
|
31
|
+
|
32
|
+
query_string = @name_args[1]
|
33
|
+
if query_string.nil?
|
34
|
+
show_usage
|
35
|
+
fatal_exit('You must specify a QUERY value (e.g. summary.overallStatus )')
|
36
|
+
end
|
37
|
+
|
38
|
+
vim_connection
|
39
|
+
|
40
|
+
dc = datacenter
|
41
|
+
folder = dc.hostFolder
|
42
|
+
|
43
|
+
pool = traverse_folders_for_pool(folder, poolname) || abort("Pool #{poolname} not found")
|
44
|
+
|
45
|
+
# split QUERY by dots, and walk the object model
|
46
|
+
query = query_string.split '.'
|
47
|
+
result = pool
|
48
|
+
query.each do |part|
|
49
|
+
message, index = part.split(/[\[\]]/)
|
50
|
+
unless result.respond_to? message.to_sym
|
51
|
+
fatal_exit("\"#{query_string}\" not recognized.")
|
52
|
+
end
|
53
|
+
|
54
|
+
result = index ? result.send(message)[index.to_i] : result.send(message)
|
55
|
+
end
|
56
|
+
puts result
|
57
|
+
end
|
58
|
+
end
|
@@ -1,32 +1,30 @@
|
|
1
|
-
#
|
2
|
-
# Author:: Ezra Pagel (<ezra@cpan.org>)
|
3
|
-
# License:: Apache License, Version 2.0
|
4
|
-
#
|
5
|
-
|
6
|
-
require 'chef/knife'
|
7
|
-
require 'chef/knife/base_vsphere_command'
|
8
|
-
|
9
|
-
# Lists all known VM templates in the configured datacenter
|
10
|
-
class Chef::Knife::VsphereTemplateList < Chef::Knife::BaseVsphereCommand
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
-
end
|
1
|
+
#
|
2
|
+
# Author:: Ezra Pagel (<ezra@cpan.org>)
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'chef/knife'
|
7
|
+
require 'chef/knife/base_vsphere_command'
|
8
|
+
|
9
|
+
# Lists all known VM templates in the configured datacenter
|
10
|
+
class Chef::Knife::VsphereTemplateList < Chef::Knife::BaseVsphereCommand
|
11
|
+
banner 'knife vsphere template list'
|
12
|
+
|
13
|
+
common_options
|
14
|
+
|
15
|
+
def run
|
16
|
+
$stdout.sync = true
|
17
|
+
$stderr.sync = true
|
18
|
+
|
19
|
+
vim_connection
|
20
|
+
|
21
|
+
base_folder = find_folder(get_config(:folder))
|
22
|
+
|
23
|
+
vms = find_all_in_folder(base_folder, RbVmomi::VIM::VirtualMachine)
|
24
|
+
.select { |v| !v.config.nil? && v.config.template == true }
|
25
|
+
|
26
|
+
vms.each do |vm|
|
27
|
+
puts "#{ui.color('Template Name', :cyan)}: #{vm.name}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
require 'chef/knife/base_vsphere_command'
|
3
|
+
|
4
|
+
# Lists all known data stores in datacenter with sizes
|
5
|
+
class Chef::Knife::VsphereVlanCreate < Chef::Knife::BaseVsphereCommand
|
6
|
+
banner 'knife vsphere vlan create NAME VID'
|
7
|
+
|
8
|
+
common_options
|
9
|
+
|
10
|
+
option :switch,
|
11
|
+
long: '--switch DVSNAME',
|
12
|
+
description: 'The DVSwitch that will hold this VLAN'
|
13
|
+
|
14
|
+
def run
|
15
|
+
$stdout.sync = true
|
16
|
+
|
17
|
+
vim_connection
|
18
|
+
net = datacenter.networkFolder
|
19
|
+
|
20
|
+
switches = net.children.select { |n| n.class == RbVmomi::VIM::VmwareDistributedVirtualSwitch }
|
21
|
+
switch = if config[:switch]
|
22
|
+
switches.find { |s| s.name == config[:switch] }
|
23
|
+
else
|
24
|
+
ui.warn 'Multiple switches found. Choosing the first switch. Use --switch to select a switch.' if switches.count > 1
|
25
|
+
switches.first
|
26
|
+
end
|
27
|
+
|
28
|
+
fatal_exit 'No switches found.' if switch.nil?
|
29
|
+
|
30
|
+
ui.info "Found #{switch.name}" if log_verbose?
|
31
|
+
switch.AddDVPortgroup_Task(spec: [add_port_spec(@name_args[0], @name_args[1])])
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def add_port_spec(name, vlan_id)
|
37
|
+
spec = RbVmomi::VIM::DVPortgroupConfigSpec(
|
38
|
+
defaultPortConfig: RbVmomi::VIM::VMwareDVSPortSetting(
|
39
|
+
vlan: RbVmomi::VIM::VmwareDistributedVirtualSwitchVlanIdSpec(
|
40
|
+
vlanId: vlan_id.to_i,
|
41
|
+
inherited: false
|
42
|
+
)
|
43
|
+
),
|
44
|
+
name: name,
|
45
|
+
numPorts: 128,
|
46
|
+
type: 'earlyBinding'
|
47
|
+
)
|
48
|
+
pp spec if log_verbose?
|
49
|
+
spec
|
50
|
+
end
|
51
|
+
end
|
@@ -1,37 +1,35 @@
|
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
dc
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
+
banner 'knife vsphere vlan list'
|
23
|
+
|
24
|
+
common_options
|
25
|
+
|
26
|
+
def run
|
27
|
+
$stdout.sync = true
|
28
|
+
|
29
|
+
vim_connection
|
30
|
+
dc = datacenter
|
31
|
+
dc.network.each do |network|
|
32
|
+
puts "#{ui.color('VLAN', :cyan)}: #{network.name}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,581 +1,834 @@
|
|
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
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
:
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
:
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
:
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
:
|
48
|
-
:
|
49
|
-
|
50
|
-
option :
|
51
|
-
:
|
52
|
-
:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
:
|
57
|
-
:
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
:
|
62
|
-
:
|
63
|
-
|
64
|
-
option :
|
65
|
-
:
|
66
|
-
:
|
67
|
-
|
68
|
-
option :
|
69
|
-
:
|
70
|
-
:
|
71
|
-
|
72
|
-
option :
|
73
|
-
:
|
74
|
-
:
|
75
|
-
|
76
|
-
option :
|
77
|
-
:
|
78
|
-
:
|
79
|
-
|
80
|
-
option :
|
81
|
-
:
|
82
|
-
:
|
83
|
-
|
84
|
-
option :
|
85
|
-
:
|
86
|
-
:
|
87
|
-
|
88
|
-
option :
|
89
|
-
:
|
90
|
-
:
|
91
|
-
|
92
|
-
option :
|
93
|
-
:
|
94
|
-
:
|
95
|
-
|
96
|
-
option :
|
97
|
-
:
|
98
|
-
:
|
99
|
-
|
100
|
-
option :
|
101
|
-
:
|
102
|
-
:
|
103
|
-
|
104
|
-
option :
|
105
|
-
:
|
106
|
-
:
|
107
|
-
|
108
|
-
option :
|
109
|
-
:
|
110
|
-
:
|
111
|
-
|
112
|
-
option :
|
113
|
-
:
|
114
|
-
:
|
115
|
-
|
116
|
-
option :
|
117
|
-
:
|
118
|
-
:
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
:
|
123
|
-
:
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
:
|
128
|
-
:
|
129
|
-
|
130
|
-
option :
|
131
|
-
:
|
132
|
-
:
|
133
|
-
|
134
|
-
option :
|
135
|
-
:
|
136
|
-
:
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
:
|
142
|
-
:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
:
|
147
|
-
:
|
148
|
-
:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
:
|
153
|
-
:
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
:
|
158
|
-
:
|
159
|
-
:
|
160
|
-
|
161
|
-
option :
|
162
|
-
:
|
163
|
-
:
|
164
|
-
:
|
165
|
-
|
166
|
-
option :
|
167
|
-
:
|
168
|
-
:
|
169
|
-
:
|
170
|
-
|
171
|
-
option :
|
172
|
-
:
|
173
|
-
:
|
174
|
-
:
|
175
|
-
|
176
|
-
option :
|
177
|
-
:
|
178
|
-
:
|
179
|
-
:
|
180
|
-
|
181
|
-
option :
|
182
|
-
:
|
183
|
-
:
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
:
|
188
|
-
:
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
:
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
:
|
198
|
-
|
199
|
-
|
200
|
-
Chef::Config[:knife][:
|
201
|
-
|
202
|
-
Chef::Config[:knife][:
|
203
|
-
|
204
|
-
|
205
|
-
option :
|
206
|
-
:
|
207
|
-
:
|
208
|
-
:
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
:
|
214
|
-
:
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
:
|
219
|
-
:
|
220
|
-
:
|
221
|
-
:
|
222
|
-
|
223
|
-
option :
|
224
|
-
:
|
225
|
-
:
|
226
|
-
:
|
227
|
-
|
228
|
-
|
229
|
-
option :
|
230
|
-
:
|
231
|
-
:
|
232
|
-
:
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
}
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
if
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
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
|
+
require 'securerandom'
|
14
|
+
require 'chef/knife/winrm_base'
|
15
|
+
|
16
|
+
# Clone an existing template into a new VM, optionally applying a customization specification.
|
17
|
+
# usage:
|
18
|
+
# knife vsphere vm clone NewNode UbuntuTemplate --cspec StaticSpec \
|
19
|
+
# --cips 192.168.0.99/24,192.168.1.99/24 \
|
20
|
+
# --chostname NODENAME --cdomain NODEDOMAIN
|
21
|
+
class Chef::Knife::VsphereVmClone < Chef::Knife::BaseVsphereCommand
|
22
|
+
banner 'knife vsphere vm clone VMNAME (options)'
|
23
|
+
|
24
|
+
include Chef::Knife::WinrmBase
|
25
|
+
include CustomizationHelper
|
26
|
+
deps do
|
27
|
+
require 'chef/json_compat'
|
28
|
+
require 'chef/knife/bootstrap'
|
29
|
+
Chef::Knife::Bootstrap.load_deps
|
30
|
+
end
|
31
|
+
|
32
|
+
common_options
|
33
|
+
|
34
|
+
option :dest_folder,
|
35
|
+
long: '--dest-folder FOLDER',
|
36
|
+
description: 'The folder into which to put the cloned VM'
|
37
|
+
|
38
|
+
option :datastore,
|
39
|
+
long: '--datastore STORE',
|
40
|
+
description: 'The datastore into which to put the cloned VM'
|
41
|
+
|
42
|
+
option :datastorecluster,
|
43
|
+
long: '--datastorecluster STORE',
|
44
|
+
description: 'The datastorecluster into which to put the cloned VM'
|
45
|
+
|
46
|
+
option :resource_pool,
|
47
|
+
long: '--resource-pool POOL',
|
48
|
+
description: 'The resource pool or cluster into which to put the cloned VM'
|
49
|
+
|
50
|
+
option :source_vm,
|
51
|
+
long: '--template TEMPLATE',
|
52
|
+
description: 'The source VM / Template to clone from'
|
53
|
+
|
54
|
+
option :linked_clone,
|
55
|
+
long: '--linked-clone',
|
56
|
+
description: 'Indicates whether to use linked clones.',
|
57
|
+
boolean: false
|
58
|
+
|
59
|
+
option :thin_provision,
|
60
|
+
long: '--thin-provision',
|
61
|
+
description: 'Indicates whether disk should be thin provisioned.',
|
62
|
+
boolean: true
|
63
|
+
|
64
|
+
option :annotation,
|
65
|
+
long: '--annotation TEXT',
|
66
|
+
description: 'Add TEXT in Notes field from annotation'
|
67
|
+
|
68
|
+
option :customization_spec,
|
69
|
+
long: '--cspec CUST_SPEC',
|
70
|
+
description: 'The name of any customization specification to apply'
|
71
|
+
|
72
|
+
option :customization_plugin,
|
73
|
+
long: '--cplugin CUST_PLUGIN_PATH',
|
74
|
+
description: 'Path to plugin that implements KnifeVspherePlugin.customize_clone_spec and/or KnifeVspherePlugin.reconfig_vm'
|
75
|
+
|
76
|
+
option :customization_plugin_data,
|
77
|
+
long: '--cplugin-data CUST_PLUGIN_DATA',
|
78
|
+
description: 'String of data to pass to the plugin. Use any format you wish.'
|
79
|
+
|
80
|
+
option :customization_vlan,
|
81
|
+
long: '--cvlan CUST_VLANS',
|
82
|
+
description: 'Comma-delimited list of VLAN names for network adapters to join'
|
83
|
+
|
84
|
+
option :customization_ips,
|
85
|
+
long: '--cips CUST_IPS',
|
86
|
+
description: 'Comma-delimited list of CIDR IPs for customization'
|
87
|
+
|
88
|
+
option :customization_dns_ips,
|
89
|
+
long: '--cdnsips CUST_DNS_IPS',
|
90
|
+
description: 'Comma-delimited list of DNS IP addresses'
|
91
|
+
|
92
|
+
option :customization_dns_suffixes,
|
93
|
+
long: '--cdnssuffix CUST_DNS_SUFFIXES',
|
94
|
+
description: 'Comma-delimited list of DNS search suffixes'
|
95
|
+
|
96
|
+
option :customization_gw,
|
97
|
+
long: '--cgw CUST_GW',
|
98
|
+
description: 'CIDR IP of gateway for customization'
|
99
|
+
|
100
|
+
option :customization_hostname,
|
101
|
+
long: '--chostname CUST_HOSTNAME',
|
102
|
+
description: 'Unqualified hostname for customization'
|
103
|
+
|
104
|
+
option :customization_domain,
|
105
|
+
long: '--cdomain CUST_DOMAIN',
|
106
|
+
description: 'Domain name for customization'
|
107
|
+
|
108
|
+
option :customization_tz,
|
109
|
+
long: '--ctz CUST_TIMEZONE',
|
110
|
+
description: "Timezone invalid 'Area/Location' format"
|
111
|
+
|
112
|
+
option :customization_cpucount,
|
113
|
+
long: '--ccpu CUST_CPU_COUNT',
|
114
|
+
description: 'Number of CPUs'
|
115
|
+
|
116
|
+
option :customization_memory,
|
117
|
+
long: '--cram CUST_MEMORY_GB',
|
118
|
+
description: 'Gigabytes of RAM'
|
119
|
+
|
120
|
+
option :power,
|
121
|
+
long: '--start',
|
122
|
+
description: 'Indicates whether to start the VM after a successful clone',
|
123
|
+
boolean: false
|
124
|
+
|
125
|
+
option :bootstrap,
|
126
|
+
long: '--bootstrap',
|
127
|
+
description: 'Indicates whether to bootstrap the VM',
|
128
|
+
boolean: false
|
129
|
+
|
130
|
+
option :environment,
|
131
|
+
long: '--environment ENVIRONMENT',
|
132
|
+
description: 'Environment to add the node to for bootstrapping'
|
133
|
+
|
134
|
+
option :fqdn,
|
135
|
+
long: '--fqdn SERVER_FQDN',
|
136
|
+
description: 'Fully qualified hostname for bootstrapping'
|
137
|
+
|
138
|
+
option :bootstrap_protocol,
|
139
|
+
long: '--bootstrap-protocol protocol',
|
140
|
+
description: 'Protocol to bootstrap windows servers. options: winrm/ssh',
|
141
|
+
proc: proc { |key| Chef::Config[:knife][:bootstrap_protocol] = key },
|
142
|
+
default: nil
|
143
|
+
|
144
|
+
option :ssh_user,
|
145
|
+
short: '-x USERNAME',
|
146
|
+
long: '--ssh-user USERNAME',
|
147
|
+
description: 'The ssh username',
|
148
|
+
default: 'root'
|
149
|
+
|
150
|
+
option :ssh_password,
|
151
|
+
short: '-P PASSWORD',
|
152
|
+
long: '--ssh-password PASSWORD',
|
153
|
+
description: 'The ssh password'
|
154
|
+
|
155
|
+
option :ssh_port,
|
156
|
+
short: '-p PORT',
|
157
|
+
long: '--ssh-port PORT',
|
158
|
+
description: 'The ssh port',
|
159
|
+
default: '22'
|
160
|
+
|
161
|
+
option :identity_file,
|
162
|
+
short: '-i IDENTITY_FILE',
|
163
|
+
long: '--identity-file IDENTITY_FILE',
|
164
|
+
description: 'The SSH identity file used for authentication'
|
165
|
+
|
166
|
+
option :chef_node_name,
|
167
|
+
short: '-N NAME',
|
168
|
+
long: '--node-name NAME',
|
169
|
+
description: 'The Chef node name for your new node'
|
170
|
+
|
171
|
+
option :prerelease,
|
172
|
+
long: '--prerelease',
|
173
|
+
description: 'Install the pre-release chef gems',
|
174
|
+
boolean: false
|
175
|
+
|
176
|
+
option :bootstrap_version,
|
177
|
+
long: '--bootstrap-version VERSION',
|
178
|
+
description: 'The version of Chef to install',
|
179
|
+
proc: proc { |v| Chef::Config[:knife][:bootstrap_version] = v }
|
180
|
+
|
181
|
+
option :bootstrap_proxy,
|
182
|
+
long: '--bootstrap-proxy PROXY_URL',
|
183
|
+
description: 'The proxy server for the node being bootstrapped',
|
184
|
+
proc: proc { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
|
185
|
+
|
186
|
+
option :bootstrap_vault_file,
|
187
|
+
long: '--bootstrap-vault-file VAULT_FILE',
|
188
|
+
description: 'A JSON file with a list of vault(s) and item(s) to be updated'
|
189
|
+
|
190
|
+
option :bootstrap_vault_json,
|
191
|
+
long: '--bootstrap-vault-json VAULT_JSON',
|
192
|
+
description: 'A JSON string with the vault(s) and item(s) to be updated'
|
193
|
+
|
194
|
+
option :bootstrap_vault_item,
|
195
|
+
long: '--bootstrap-vault-item VAULT_ITEM',
|
196
|
+
description: 'A single vault and item to update as "vault:item"',
|
197
|
+
proc: proc { |i|
|
198
|
+
(vault, item) = i.split(/:/)
|
199
|
+
Chef::Config[:knife][:bootstrap_vault_item] ||= {}
|
200
|
+
Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
|
201
|
+
Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
|
202
|
+
Chef::Config[:knife][:bootstrap_vault_item]
|
203
|
+
}
|
204
|
+
|
205
|
+
option :distro,
|
206
|
+
short: '-d DISTRO',
|
207
|
+
long: '--distro DISTRO',
|
208
|
+
description: 'Bootstrap a distro using a template; default is "chef-full"',
|
209
|
+
proc: proc { |d| Chef::Config[:knife][:distro] = d },
|
210
|
+
default: 'chef-full'
|
211
|
+
|
212
|
+
option :template_file,
|
213
|
+
long: '--template-file TEMPLATE',
|
214
|
+
description: 'Full path to location of template to use'
|
215
|
+
|
216
|
+
option :run_list,
|
217
|
+
short: '-r RUN_LIST',
|
218
|
+
long: '--run-list RUN_LIST',
|
219
|
+
description: 'Comma separated list of roles/recipes to apply',
|
220
|
+
proc: -> (o) { o.split(/[\s,]+/) },
|
221
|
+
default: []
|
222
|
+
|
223
|
+
option :secret_file,
|
224
|
+
long: '--secret-file SECRET_FILE',
|
225
|
+
description: 'A file containing the secret key to use to encrypt data bag item values',
|
226
|
+
proc: ->(secret_file) { Chef::Config[:knife][:secret_file] = secret_file }
|
227
|
+
|
228
|
+
# rubocop:disable Style/Blocks
|
229
|
+
option :hint,
|
230
|
+
long: '--hint HINT_NAME[=HINT_FILE]',
|
231
|
+
description: 'Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.',
|
232
|
+
proc: proc { |h|
|
233
|
+
Chef::Config[:knife][:hints] ||= {}
|
234
|
+
name, path = h.split('=')
|
235
|
+
Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : {}
|
236
|
+
},
|
237
|
+
default: ''
|
238
|
+
# rubocop:enable Style/Blocks
|
239
|
+
|
240
|
+
option :no_host_key_verify,
|
241
|
+
long: '--no-host-key-verify',
|
242
|
+
description: 'Disable host key verification',
|
243
|
+
boolean: true
|
244
|
+
|
245
|
+
option :first_boot_attributes,
|
246
|
+
short: '-j JSON_ATTRIBS',
|
247
|
+
long: '--json-attributes',
|
248
|
+
description: 'A JSON string to be added to the first run of chef-client',
|
249
|
+
proc: ->(o) { JSON.parse(o) },
|
250
|
+
default: {}
|
251
|
+
|
252
|
+
option :disable_customization,
|
253
|
+
long: '--disable-customization',
|
254
|
+
description: 'Disable default customization',
|
255
|
+
boolean: true,
|
256
|
+
default: false
|
257
|
+
|
258
|
+
option :log_level,
|
259
|
+
short: '-l LEVEL',
|
260
|
+
long: '--log_level',
|
261
|
+
description: 'Set the log level (debug, info, warn, error, fatal) for chef-client',
|
262
|
+
proc: ->(l) { l.to_sym }
|
263
|
+
|
264
|
+
option :mark_as_template,
|
265
|
+
long: '--mark_as_template',
|
266
|
+
description: 'Indicates whether to mark the new vm as a template',
|
267
|
+
boolean: false
|
268
|
+
|
269
|
+
option :random_vmname,
|
270
|
+
long: '--random-vmname',
|
271
|
+
description: 'Creates a random VMNAME starts with vm-XXXXXXXX',
|
272
|
+
boolean: false
|
273
|
+
|
274
|
+
option :random_vmname_prefix,
|
275
|
+
long: '--random-vmname-prefix PREFIX',
|
276
|
+
description: 'Change the VMNAME prefix',
|
277
|
+
default: 'vm-'
|
278
|
+
|
279
|
+
option :sysprep_timeout,
|
280
|
+
long: '--sysprep_timeout TIMEOUT',
|
281
|
+
description: 'Wait TIMEOUT seconds for sysprep event before continuing with bootstrap',
|
282
|
+
default: 600
|
283
|
+
|
284
|
+
def run
|
285
|
+
$stdout.sync = true
|
286
|
+
|
287
|
+
unless using_supplied_hostname? ^ using_random_hostname?
|
288
|
+
show_usage
|
289
|
+
fatal_exit('You must specify a virtual machine name OR use --random-vmname')
|
290
|
+
end
|
291
|
+
|
292
|
+
config[:chef_node_name] = vmname unless get_config(:chef_node_name)
|
293
|
+
config[:vmname] = vmname
|
294
|
+
|
295
|
+
vim = vim_connection
|
296
|
+
vim.serviceContent.virtualDiskManager
|
297
|
+
|
298
|
+
dc = datacenter
|
299
|
+
|
300
|
+
src_folder = find_folder(get_config(:folder)) || dc.vmFolder
|
301
|
+
|
302
|
+
abort '--template or knife[:source_vm] must be specified' unless config[:source_vm]
|
303
|
+
|
304
|
+
src_vm = find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, config[:source_vm]) ||
|
305
|
+
abort('VM/Template not found')
|
306
|
+
|
307
|
+
create_delta_disk(src_vm) if get_config(:linked_clone)
|
308
|
+
|
309
|
+
clone_spec = generate_clone_spec(src_vm.config)
|
310
|
+
|
311
|
+
cust_folder = config[:dest_folder] || get_config(:folder)
|
312
|
+
|
313
|
+
dest_folder = cust_folder.nil? ? src_vm.vmFolder : find_folder(cust_folder)
|
314
|
+
|
315
|
+
task = src_vm.CloneVM_Task(folder: dest_folder, name: vmname, spec: clone_spec)
|
316
|
+
puts "Cloning template #{config[:source_vm]} to new VM #{vmname}"
|
317
|
+
task.wait_for_completion
|
318
|
+
puts "Finished creating virtual machine #{vmname}"
|
319
|
+
|
320
|
+
if customization_plugin && customization_plugin.respond_to?(:reconfig_vm)
|
321
|
+
target_vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) || abort("VM could not be found in #{dest_folder}")
|
322
|
+
customization_plugin.reconfig_vm(target_vm)
|
323
|
+
end
|
324
|
+
|
325
|
+
return if get_config(:mark_as_template)
|
326
|
+
if get_config(:power) || get_config(:bootstrap)
|
327
|
+
vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) ||
|
328
|
+
fatal_exit("VM #{vmname} not found")
|
329
|
+
vm.PowerOnVM_Task.wait_for_completion
|
330
|
+
puts "Powered on virtual machine #{vmname}"
|
331
|
+
end
|
332
|
+
|
333
|
+
return unless get_config(:bootstrap)
|
334
|
+
sleep 2 until vm.guest.ipAddress
|
335
|
+
|
336
|
+
connect_host = config[:fqdn] = config[:fqdn] ? get_config(:fqdn) : vm.guest.ipAddress
|
337
|
+
Chef::Log.debug("Connect Host for Bootstrap: #{connect_host}")
|
338
|
+
connect_port = get_config(:ssh_port)
|
339
|
+
protocol = get_config(:bootstrap_protocol)
|
340
|
+
if windows?(src_vm.config)
|
341
|
+
protocol ||= 'winrm'
|
342
|
+
# Set distro to windows-chef-client-msi
|
343
|
+
config[:distro] = 'windows-chef-client-msi' if config[:distro].nil? || config[:distro] == 'chef-full'
|
344
|
+
unless config[:disable_customization]
|
345
|
+
# Wait for customization to complete
|
346
|
+
# TODO: Figure out how to find the customization complete event from the vsphere logs. The
|
347
|
+
# customization can take up to 10 minutes to complete from what I have seen perhaps
|
348
|
+
# even longer. For now I am simply sleeping, but if anyone knows how to do this
|
349
|
+
# better fix it.
|
350
|
+
puts 'Waiting for customization to complete...'
|
351
|
+
CustomizationHelper.wait_for_sysprep(vm, vim, get_config(:sysprep_timeout), 10)
|
352
|
+
puts 'Customization Complete'
|
353
|
+
sleep 2 until vm.guest.ipAddress
|
354
|
+
connect_host = config[:fqdn] = config[:fqdn] ? get_config(:fqdn) : vm.guest.ipAddress
|
355
|
+
end
|
356
|
+
wait_for_access(connect_host, connect_port, protocol)
|
357
|
+
ssh_override_winrm
|
358
|
+
bootstrap_for_windows_node.run
|
359
|
+
else
|
360
|
+
protocol ||= 'ssh'
|
361
|
+
wait_for_access(connect_host, connect_port, protocol)
|
362
|
+
ssh_override_winrm
|
363
|
+
bootstrap_for_node.run
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def wait_for_access(connect_host, connect_port, protocol)
|
368
|
+
if protocol == 'winrm'
|
369
|
+
load_winrm_deps
|
370
|
+
connect_port = get_config(:winrm_port)
|
371
|
+
print "\n#{ui.color('Waiting for winrm access to become available', :magenta)}"
|
372
|
+
print('.') until tcp_test_winrm(connect_host, connect_port) do
|
373
|
+
sleep 10
|
374
|
+
puts('done')
|
375
|
+
end
|
376
|
+
else
|
377
|
+
print "\n#{ui.color('Waiting for sshd access to become available', :magenta)}"
|
378
|
+
# If FreeSSHd, winsshd etc are available
|
379
|
+
print('.') until tcp_test_ssh(connect_host, connect_port) do
|
380
|
+
sleep 10
|
381
|
+
puts('done')
|
382
|
+
end
|
383
|
+
end
|
384
|
+
connect_port
|
385
|
+
end
|
386
|
+
|
387
|
+
def create_delta_disk(src_vm)
|
388
|
+
disks = src_vm.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk)
|
389
|
+
disks.select { |disk| disk.backing.parent.nil? }.each do |disk|
|
390
|
+
spec = {
|
391
|
+
deviceChange: [
|
392
|
+
{
|
393
|
+
operation: :remove,
|
394
|
+
device: disk
|
395
|
+
},
|
396
|
+
{
|
397
|
+
operation: :add,
|
398
|
+
fileOperation: :create,
|
399
|
+
device: disk.dup.tap do |new_disk|
|
400
|
+
new_disk.backing = new_disk.backing.dup
|
401
|
+
new_disk.backing.fileName = "[#{disk.backing.datastore.name}]"
|
402
|
+
new_disk.backing.parent = disk.backing
|
403
|
+
end
|
404
|
+
}
|
405
|
+
]
|
406
|
+
}
|
407
|
+
src_vm.ReconfigVM_Task(spec: spec).wait_for_completion
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# Builds a CloneSpec
|
412
|
+
def generate_clone_spec(src_config)
|
413
|
+
rspec = nil
|
414
|
+
if get_config(:resource_pool)
|
415
|
+
rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(pool: find_pool(get_config(:resource_pool)))
|
416
|
+
else
|
417
|
+
dc = datacenter
|
418
|
+
hosts = traverse_folders_for_computeresources(dc.hostFolder)
|
419
|
+
fatal_exit('No ComputeResource found - Use --resource-pool to specify a resource pool or a cluster') if hosts.empty?
|
420
|
+
hosts.reject!(&:nil?)
|
421
|
+
hosts.reject! { |host| host.host.all? { |h| h.runtime.inMaintenanceMode } }
|
422
|
+
fatal_exit 'All hosts in maintenance mode!' if hosts.empty?
|
423
|
+
|
424
|
+
if get_config(:datastore)
|
425
|
+
hosts.reject! { |host| !host.datastore.include?(find_datastore(get_config(:datastore))) }
|
426
|
+
end
|
427
|
+
|
428
|
+
fatal_exit "No hosts have the requested Datastore available! #{get_config(:datastore)}" if hosts.empty?
|
429
|
+
|
430
|
+
if get_config(:datastorecluster)
|
431
|
+
hosts.reject! { |host| !host.datastore.include?(find_datastorecluster(get_config(:datastorecluster))) }
|
432
|
+
end
|
433
|
+
|
434
|
+
fatal_exit "No hosts have the requested DatastoreCluster available! #{get_config(:datastorecluster)}" if hosts.empty?
|
435
|
+
|
436
|
+
if get_config(:customization_vlan)
|
437
|
+
hosts.reject! { |host| !host.network.include?(find_network(get_config(:customization_vlan))) }
|
438
|
+
end
|
439
|
+
|
440
|
+
fatal_exit "No hosts have the requested Network available! #{get_config(:customization_vlan)}" if hosts.empty?
|
441
|
+
|
442
|
+
rp = hosts.first.resourcePool
|
443
|
+
rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(pool: rp)
|
444
|
+
end
|
445
|
+
|
446
|
+
if get_config(:linked_clone)
|
447
|
+
rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(diskMoveType: :moveChildMostDiskBacking)
|
448
|
+
end
|
449
|
+
|
450
|
+
if get_config(:datastore) && get_config(:datastorecluster)
|
451
|
+
abort 'Please select either datastore or datastorecluster'
|
452
|
+
end
|
453
|
+
|
454
|
+
if get_config(:datastore)
|
455
|
+
rspec.datastore = find_datastore(get_config(:datastore))
|
456
|
+
end
|
457
|
+
|
458
|
+
if get_config(:datastorecluster)
|
459
|
+
dsc = find_datastorecluster(get_config(:datastorecluster))
|
460
|
+
|
461
|
+
dsc.childEntity.each do |store|
|
462
|
+
if rspec.datastore.nil? || rspec.datastore.summary[:freeSpace] < store.summary[:freeSpace]
|
463
|
+
rspec.datastore = store
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
if get_config(:thin_provision)
|
469
|
+
rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(transform: :sparse, pool: find_pool(get_config(:resource_pool)))
|
470
|
+
end
|
471
|
+
|
472
|
+
is_template = !get_config(:mark_as_template).nil?
|
473
|
+
clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(location: rspec, powerOn: false, template: is_template)
|
474
|
+
|
475
|
+
clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(deviceChange: [])
|
476
|
+
|
477
|
+
if get_config(:annotation)
|
478
|
+
clone_spec.config.annotation = get_config(:annotation)
|
479
|
+
end
|
480
|
+
|
481
|
+
if get_config(:customization_cpucount)
|
482
|
+
clone_spec.config.numCPUs = get_config(:customization_cpucount)
|
483
|
+
end
|
484
|
+
|
485
|
+
if get_config(:customization_memory)
|
486
|
+
clone_spec.config.memoryMB = Integer(get_config(:customization_memory)) * 1024
|
487
|
+
end
|
488
|
+
|
489
|
+
if get_config(:customization_vlan)
|
490
|
+
vlan_list = get_config(:customization_vlan).split(',')
|
491
|
+
networks = vlan_list.map { |vlan| find_network(vlan) }
|
492
|
+
|
493
|
+
cards = src_config.hardware.device.grep(RbVmomi::VIM::VirtualEthernetCard)
|
494
|
+
|
495
|
+
networks.each_with_index do |network, index|
|
496
|
+
card = cards[index] || abort("Can't find source network card to customize for vlan #{vlan_list[index]}")
|
497
|
+
begin
|
498
|
+
switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(switchUuid: network.config.distributedVirtualSwitch.uuid, portgroupKey: network.key)
|
499
|
+
card.backing.port = switch_port
|
500
|
+
rescue
|
501
|
+
# not connected to a distibuted switch?
|
502
|
+
card.backing = RbVmomi::VIM::VirtualEthernetCardNetworkBackingInfo(network: network, deviceName: network.name)
|
503
|
+
end
|
504
|
+
dev_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(device: card, operation: 'edit')
|
505
|
+
clone_spec.config.deviceChange.push dev_spec
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
if get_config(:customization_spec)
|
510
|
+
csi = find_customization(get_config(:customization_spec)) ||
|
511
|
+
fatal_exit("failed to find customization specification named #{get_config(:customization_spec)}")
|
512
|
+
|
513
|
+
cust_spec = csi.spec
|
514
|
+
else
|
515
|
+
global_ipset = RbVmomi::VIM.CustomizationGlobalIPSettings
|
516
|
+
cust_spec = RbVmomi::VIM.CustomizationSpec(globalIPSettings: global_ipset)
|
517
|
+
end
|
518
|
+
|
519
|
+
if get_config(:customization_dns_ips)
|
520
|
+
cust_spec.globalIPSettings.dnsServerList = get_config(:customization_dns_ips).split(',')
|
521
|
+
end
|
522
|
+
|
523
|
+
if get_config(:customization_dns_suffixes)
|
524
|
+
cust_spec.globalIPSettings.dnsSuffixList = get_config(:customization_dns_suffixes).split(',')
|
525
|
+
end
|
526
|
+
|
527
|
+
if config[:customization_ips]
|
528
|
+
if get_config(:customization_gw)
|
529
|
+
cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i, get_config(:customization_gw)) }
|
530
|
+
else
|
531
|
+
cust_spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
unless get_config(:disable_customization)
|
536
|
+
use_ident = !config[:customization_hostname].nil? || !get_config(:customization_domain).nil? || cust_spec.identity.nil?
|
537
|
+
|
538
|
+
if use_ident
|
539
|
+
hostname = if config[:customization_hostname]
|
540
|
+
config[:customization_hostname]
|
541
|
+
else
|
542
|
+
config[:vmname]
|
543
|
+
end
|
544
|
+
if windows?(src_config)
|
545
|
+
identification = RbVmomi::VIM.CustomizationIdentification(
|
546
|
+
joinWorkgroup: cust_spec.identity.identification.joinWorkgroup
|
547
|
+
)
|
548
|
+
license_file_print_data = RbVmomi::VIM.CustomizationLicenseFilePrintData(
|
549
|
+
autoMode: cust_spec.identity.licenseFilePrintData.autoMode
|
550
|
+
)
|
551
|
+
|
552
|
+
user_data = RbVmomi::VIM.CustomizationUserData(
|
553
|
+
fullName: cust_spec.identity.userData.fullName,
|
554
|
+
orgName: cust_spec.identity.userData.orgName,
|
555
|
+
productId: cust_spec.identity.userData.productId,
|
556
|
+
computerName: cust_spec.identity.userData.computerName
|
557
|
+
)
|
558
|
+
gui_unattended = RbVmomi::VIM.CustomizationGuiUnattended(
|
559
|
+
autoLogon: cust_spec.identity.guiUnattended.autoLogon,
|
560
|
+
autoLogonCount: cust_spec.identity.guiUnattended.autoLogonCount,
|
561
|
+
password: RbVmomi::VIM.CustomizationPassword(
|
562
|
+
plainText: cust_spec.identity.guiUnattended.password.plainText,
|
563
|
+
value: cust_spec.identity.guiUnattended.password.value
|
564
|
+
),
|
565
|
+
timeZone: cust_spec.identity.guiUnattended.timeZone
|
566
|
+
)
|
567
|
+
runonce = RbVmomi::VIM.CustomizationGuiRunOnce(
|
568
|
+
commandList: ['cust_spec.identity.guiUnattended.commandList']
|
569
|
+
)
|
570
|
+
ident = RbVmomi::VIM.CustomizationSysprep
|
571
|
+
ident.guiRunOnce = runonce
|
572
|
+
ident.guiUnattended = gui_unattended
|
573
|
+
ident.identification = identification
|
574
|
+
ident.licenseFilePrintData = license_file_print_data
|
575
|
+
ident.userData = user_data
|
576
|
+
cust_spec.identity = ident
|
577
|
+
elsif linux?(src_config)
|
578
|
+
ident = RbVmomi::VIM.CustomizationLinuxPrep
|
579
|
+
ident.hostName = RbVmomi::VIM.CustomizationFixedName(name: hostname)
|
580
|
+
|
581
|
+
if get_config(:customization_domain)
|
582
|
+
ident.domain = get_config(:customization_domain)
|
583
|
+
else
|
584
|
+
ident.domain = ''
|
585
|
+
end
|
586
|
+
cust_spec.identity = ident
|
587
|
+
else
|
588
|
+
ui.error('Customization only supports Linux and Windows currently.')
|
589
|
+
exit 1
|
590
|
+
end
|
591
|
+
end
|
592
|
+
clone_spec.customization = cust_spec
|
593
|
+
|
594
|
+
if customization_plugin && customization_plugin.respond_to?(:customize_clone_spec)
|
595
|
+
clone_spec = customization_plugin.customize_clone_spec(src_config, clone_spec)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
clone_spec
|
599
|
+
end
|
600
|
+
|
601
|
+
# Loads the customization plugin if one was specified
|
602
|
+
# @return [KnifeVspherePlugin] the loaded and initialized plugin or nil
|
603
|
+
def customization_plugin
|
604
|
+
if @customization_plugin.nil?
|
605
|
+
cplugin_path = get_config(:customization_plugin)
|
606
|
+
if cplugin_path
|
607
|
+
if File.exist? cplugin_path
|
608
|
+
require cplugin_path
|
609
|
+
else
|
610
|
+
abort "Customization plugin could not be found at #{cplugin_path}"
|
611
|
+
end
|
612
|
+
|
613
|
+
if Object.const_defined? 'KnifeVspherePlugin'
|
614
|
+
@customization_plugin = Object.const_get('KnifeVspherePlugin').new
|
615
|
+
cplugin_data = get_config(:customization_plugin_data)
|
616
|
+
if cplugin_data
|
617
|
+
if @customization_plugin.respond_to?(:data=)
|
618
|
+
@customization_plugin.data = cplugin_data
|
619
|
+
else
|
620
|
+
abort 'Customization plugin has no :data= accessor to receive the --cplugin-data argument. Define both or neither.'
|
621
|
+
end
|
622
|
+
end
|
623
|
+
else
|
624
|
+
abort "KnifeVspherePlugin class is not defined in #{cplugin_path}"
|
625
|
+
end
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
@customization_plugin
|
630
|
+
end
|
631
|
+
|
632
|
+
# Retrieves a CustomizationSpecItem that matches the supplied name
|
633
|
+
# @param vim [Connection] VI Connection to use
|
634
|
+
# @param name [String] name of customization
|
635
|
+
# @return [RbVmomi::VIM::CustomizationSpecItem]
|
636
|
+
def find_customization(name)
|
637
|
+
csm = config[:vim].serviceContent.customizationSpecManager
|
638
|
+
csm.GetCustomizationSpec(name: name)
|
639
|
+
end
|
640
|
+
|
641
|
+
# Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
|
642
|
+
# @param ip [String] Any static IP address to use, or "dhcp" for DHCP
|
643
|
+
# @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
|
644
|
+
# @return [RbVmomi::VIM::CustomizationIPSettings]
|
645
|
+
def generate_adapter_map(ip = nil, gw = nil)
|
646
|
+
settings = RbVmomi::VIM.CustomizationIPSettings
|
647
|
+
|
648
|
+
if ip.nil? || ip.downcase == 'dhcp'
|
649
|
+
settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator.new
|
650
|
+
else
|
651
|
+
cidr_ip = NetAddr::CIDR.create(ip)
|
652
|
+
settings.ip = RbVmomi::VIM::CustomizationFixedIp(ipAddress: cidr_ip.ip)
|
653
|
+
settings.subnetMask = cidr_ip.netmask_ext
|
654
|
+
|
655
|
+
# TODO: want to confirm gw/ip are in same subnet?
|
656
|
+
# Only set gateway on first IP.
|
657
|
+
if config[:customization_ips].split(',').first == ip
|
658
|
+
if gw.nil?
|
659
|
+
settings.gateway = [cidr_ip.network(Objectify: true).next_ip]
|
660
|
+
else
|
661
|
+
gw_cidr = NetAddr::CIDR.create(gw)
|
662
|
+
settings.gateway = [gw_cidr.ip]
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
|
668
|
+
adapter_map.adapter = settings
|
669
|
+
adapter_map
|
670
|
+
end
|
671
|
+
|
672
|
+
def bootstrap_common_params(bootstrap)
|
673
|
+
bootstrap.config[:run_list] = config[:run_list]
|
674
|
+
bootstrap.config[:bootstrap_version] = get_config(:bootstrap_version)
|
675
|
+
bootstrap.config[:distro] = get_config(:distro)
|
676
|
+
bootstrap.config[:template_file] = get_config(:template_file)
|
677
|
+
bootstrap.config[:environment] = get_config(:environment)
|
678
|
+
bootstrap.config[:prerelease] = get_config(:prerelease)
|
679
|
+
bootstrap.config[:first_boot_attributes] = get_config(:first_boot_attributes)
|
680
|
+
bootstrap.config[:hint] = get_config(:hint)
|
681
|
+
bootstrap.config[:chef_node_name] = get_config(:chef_node_name)
|
682
|
+
bootstrap.config[:bootstrap_vault_file] = get_config(:bootstrap_vault_file)
|
683
|
+
bootstrap.config[:bootstrap_vault_json] = get_config(:bootstrap_vault_json)
|
684
|
+
bootstrap.config[:bootstrap_vault_item] = get_config(:bootstrap_vault_item)
|
685
|
+
# may be needed for vpc mode
|
686
|
+
bootstrap.config[:no_host_key_verify] = get_config(:no_host_key_verify)
|
687
|
+
bootstrap
|
688
|
+
end
|
689
|
+
|
690
|
+
def bootstrap_for_windows_node
|
691
|
+
Chef::Knife::Bootstrap.load_deps
|
692
|
+
if get_config(:bootstrap_protocol) == 'winrm' || get_config(:bootstrap_protocol).nil?
|
693
|
+
bootstrap = Chef::Knife::BootstrapWindowsWinrm.new
|
694
|
+
bootstrap.name_args = [config[:fqdn]]
|
695
|
+
bootstrap.config[:winrm_user] = get_config(:winrm_user)
|
696
|
+
bootstrap.config[:winrm_password] = get_config(:winrm_password)
|
697
|
+
bootstrap.config[:winrm_transport] = get_config(:winrm_transport)
|
698
|
+
bootstrap.config[:winrm_port] = get_config(:winrm_port)
|
699
|
+
elsif get_config(:bootstrap_protocol) == 'ssh'
|
700
|
+
bootstrap = Chef::Knife::BootstrapWindowsSsh.new
|
701
|
+
bootstrap.config[:ssh_user] = get_config(:ssh_user)
|
702
|
+
bootstrap.config[:ssh_password] = get_config(:ssh_password)
|
703
|
+
bootstrap.config[:ssh_port] = get_config(:ssh_port)
|
704
|
+
else
|
705
|
+
ui.error('Unsupported Bootstrapping Protocol. Supports : winrm, ssh')
|
706
|
+
exit 1
|
707
|
+
end
|
708
|
+
bootstrap_common_params(bootstrap)
|
709
|
+
end
|
710
|
+
|
711
|
+
def bootstrap_for_node
|
712
|
+
Chef::Knife::Bootstrap.load_deps
|
713
|
+
bootstrap = Chef::Knife::Bootstrap.new
|
714
|
+
bootstrap.name_args = [config[:fqdn]]
|
715
|
+
bootstrap.config[:secret_file] = get_config(:secret_file)
|
716
|
+
bootstrap.config[:ssh_user] = get_config(:ssh_user)
|
717
|
+
bootstrap.config[:ssh_password] = get_config(:ssh_password)
|
718
|
+
bootstrap.config[:ssh_port] = get_config(:ssh_port)
|
719
|
+
bootstrap.config[:identity_file] = get_config(:identity_file)
|
720
|
+
bootstrap.config[:use_sudo] = true unless get_config(:ssh_user) == 'root'
|
721
|
+
bootstrap.config[:log_level] = get_config(:log_level)
|
722
|
+
bootstrap_common_params(bootstrap)
|
723
|
+
end
|
724
|
+
|
725
|
+
def ssh_override_winrm
|
726
|
+
# unchanged ssh_user and changed winrm_user, override ssh_user
|
727
|
+
if get_config(:ssh_user).eql?(options[:ssh_user][:default]) &&
|
728
|
+
!get_config(:winrm_user).eql?(options[:winrm_user][:default])
|
729
|
+
config[:ssh_user] = get_config(:winrm_user)
|
730
|
+
end
|
731
|
+
|
732
|
+
# unchanged ssh_port and changed winrm_port, override ssh_port
|
733
|
+
if get_config(:ssh_port).eql?(options[:ssh_port][:default]) &&
|
734
|
+
!get_config(:winrm_port).eql?(options[:winrm_port][:default])
|
735
|
+
config[:ssh_port] = get_config(:winrm_port)
|
736
|
+
end
|
737
|
+
|
738
|
+
# unset ssh_password and set winrm_password, override ssh_password
|
739
|
+
if get_config(:ssh_password).nil? &&
|
740
|
+
!get_config(:winrm_password).nil?
|
741
|
+
config[:ssh_password] = get_config(:winrm_password)
|
742
|
+
end
|
743
|
+
|
744
|
+
# unset identity_file and set kerberos_keytab_file, override identity_file
|
745
|
+
return unless get_config(:identity_file).nil? && !get_config(:kerberos_keytab_file).nil?
|
746
|
+
|
747
|
+
config[:identity_file] = get_config(:kerberos_keytab_file)
|
748
|
+
end
|
749
|
+
|
750
|
+
def tcp_test_ssh(hostname, ssh_port)
|
751
|
+
tcp_socket = TCPSocket.new(hostname, ssh_port)
|
752
|
+
readable = IO.select([tcp_socket], nil, nil, 5)
|
753
|
+
if readable
|
754
|
+
ssh_banner = tcp_socket.gets
|
755
|
+
if ssh_banner.nil? || ssh_banner.empty?
|
756
|
+
false
|
757
|
+
else
|
758
|
+
Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{ssh_banner}")
|
759
|
+
yield
|
760
|
+
true
|
761
|
+
end
|
762
|
+
else
|
763
|
+
false
|
764
|
+
end
|
765
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
|
766
|
+
Chef::Log.debug("ssh failed to connect: #{hostname}")
|
767
|
+
sleep 2
|
768
|
+
false
|
769
|
+
rescue Errno::EPERM, Errno::ETIMEDOUT
|
770
|
+
Chef::Log.debug("ssh timed out: #{hostname}")
|
771
|
+
false
|
772
|
+
rescue Errno::ECONNRESET
|
773
|
+
Chef::Log.debug("ssh reset its connection: #{hostname}")
|
774
|
+
sleep 2
|
775
|
+
false
|
776
|
+
ensure
|
777
|
+
tcp_socket && tcp_socket.close
|
778
|
+
end
|
779
|
+
|
780
|
+
def tcp_test_winrm(hostname, port)
|
781
|
+
tcp_socket = TCPSocket.new(hostname, port)
|
782
|
+
yield
|
783
|
+
true
|
784
|
+
rescue SocketError
|
785
|
+
sleep 2
|
786
|
+
false
|
787
|
+
rescue Errno::ETIMEDOUT
|
788
|
+
false
|
789
|
+
rescue Errno::EPERM
|
790
|
+
false
|
791
|
+
rescue Errno::ECONNREFUSED
|
792
|
+
sleep 2
|
793
|
+
false
|
794
|
+
rescue Errno::EHOSTUNREACH
|
795
|
+
sleep 2
|
796
|
+
false
|
797
|
+
rescue Errno::ENETUNREACH
|
798
|
+
sleep 2
|
799
|
+
false
|
800
|
+
ensure
|
801
|
+
tcp_socket && tcp_socket.close
|
802
|
+
end
|
803
|
+
|
804
|
+
def load_winrm_deps
|
805
|
+
require 'winrm'
|
806
|
+
require 'em-winrm'
|
807
|
+
require 'chef/knife/winrm'
|
808
|
+
require 'chef/knife/bootstrap_windows_winrm'
|
809
|
+
require 'chef/knife/bootstrap_windows_ssh'
|
810
|
+
require 'chef/knife/core/windows_bootstrap_context'
|
811
|
+
end
|
812
|
+
|
813
|
+
private
|
814
|
+
|
815
|
+
def vmname
|
816
|
+
supplied_hostname || random_hostname
|
817
|
+
end
|
818
|
+
|
819
|
+
def using_random_hostname?
|
820
|
+
config[:random_vmname]
|
821
|
+
end
|
822
|
+
|
823
|
+
def using_supplied_hostname?
|
824
|
+
!supplied_hostname.nil?
|
825
|
+
end
|
826
|
+
|
827
|
+
def supplied_hostname
|
828
|
+
@name_args[0]
|
829
|
+
end
|
830
|
+
|
831
|
+
def random_hostname
|
832
|
+
@random_hostname ||= config[:random_vmname_prefix] + SecureRandom.hex(4)
|
833
|
+
end
|
834
|
+
end
|