rvc 1.7.0 → 1.8.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.
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/lib/rvc/completion.rb +1 -1
- data/lib/rvc/extensions/ClusterComputeResource.rb +11 -0
- data/lib/rvc/extensions/ComputeResource.rb +5 -1
- data/lib/rvc/extensions/Datacenter.rb +1 -1
- data/lib/rvc/extensions/HostSystem.rb +15 -0
- data/lib/rvc/extensions/VirtualMachine.rb +7 -2
- data/lib/rvc/fs.rb +2 -2
- data/lib/rvc/inventory.rb +9 -1
- data/lib/rvc/modules/basic.rb +4 -1
- data/lib/rvc/modules/cluster.rb +99 -38
- data/lib/rvc/modules/connection.rb +2 -0
- data/lib/rvc/modules/device.rb +37 -1
- data/lib/rvc/modules/diagnostics.rb +119 -11
- data/lib/rvc/modules/find.rb +1 -1
- data/lib/rvc/modules/host.rb +116 -4
- data/lib/rvc/modules/perf.rb +53 -7
- data/lib/rvc/modules/snapshot.rb +7 -1
- data/lib/rvc/modules/spbm.rb +728 -0
- data/lib/rvc/modules/syslog.rb +103 -0
- data/lib/rvc/modules/vds.rb +59 -2
- data/lib/rvc/modules/vim.rb +1 -1
- data/lib/rvc/modules/vm.rb +70 -7
- data/lib/rvc/modules/vm_guest.rb +190 -91
- data/lib/rvc/modules/vnc.rb +29 -5
- data/lib/rvc/modules/vsan.rb +4105 -0
- data/lib/rvc/util.rb +31 -11
- metadata +7 -3
data/lib/rvc/modules/find.rb
CHANGED
@@ -72,7 +72,7 @@ def leaves roots, types = []
|
|
72
72
|
nodes.each do |node|
|
73
73
|
if (node.class.traverse? or roots.member? node) and
|
74
74
|
(types & (node.field('type') || [])).empty?
|
75
|
-
node.
|
75
|
+
node.rvc_children.each { |k,v| v.rvc_link(node, k); new_nodes << v }
|
76
76
|
else
|
77
77
|
leaves << node
|
78
78
|
end
|
data/lib/rvc/modules/host.rb
CHANGED
@@ -19,6 +19,10 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require 'rvc/vim'
|
22
|
+
begin
|
23
|
+
require 'net/ssh'
|
24
|
+
rescue LoadError
|
25
|
+
end
|
22
26
|
|
23
27
|
opts :reboot do
|
24
28
|
summary "Reboot hosts"
|
@@ -49,6 +53,37 @@ def reboot hosts, opts
|
|
49
53
|
end
|
50
54
|
end
|
51
55
|
|
56
|
+
opts :restart_services do
|
57
|
+
summary "Restart all services in hosts"
|
58
|
+
arg :host, nil, :lookup => VIM::HostSystem, :multi => true
|
59
|
+
opt :password, "Host Password", :default => ''
|
60
|
+
end
|
61
|
+
|
62
|
+
def restart_services hosts, opts
|
63
|
+
|
64
|
+
hosts.each do |host|
|
65
|
+
Net::SSH.start(host.name, "root", :password => opts[:password], :paranoid => false) do |ssh|
|
66
|
+
cmd = "/sbin/chkconfig usbarbitrator off"
|
67
|
+
puts "Running #{cmd}"
|
68
|
+
out = ssh_exec!(ssh,cmd)
|
69
|
+
if out[2] != 0
|
70
|
+
puts "Failed to execute #{cmd} on host #{host.name}"
|
71
|
+
puts out[1]
|
72
|
+
end
|
73
|
+
|
74
|
+
cmd = "/sbin/services.sh restart > /tmp/restart_services.log 2>&1"
|
75
|
+
puts "Running #{cmd}"
|
76
|
+
out = ssh_exec!(ssh,cmd)
|
77
|
+
if out[2] != 0
|
78
|
+
puts "Failed to restart all services on host #{host.name}"
|
79
|
+
puts out[1]
|
80
|
+
else
|
81
|
+
puts "Host #{host.name} restarted all services"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
52
87
|
|
53
88
|
opts :evacuate do
|
54
89
|
summary "vMotion all VMs away from this host (experimental)"
|
@@ -154,6 +189,7 @@ opts :add_iscsi_target do
|
|
154
189
|
arg :host, nil, :lookup => VIM::HostSystem, :multi => true
|
155
190
|
opt :address, "Address of iSCSI server", :short => 'a', :type => :string, :required => true
|
156
191
|
opt :iqn, "IQN of iSCSI target", :short => 'i', :type => :string, :required => true
|
192
|
+
opt :dynamic_target, "Use Dynamic target Discovery", :short => 'd', :type => :boolean
|
157
193
|
end
|
158
194
|
|
159
195
|
def add_iscsi_target hosts, opts
|
@@ -162,10 +198,27 @@ def add_iscsi_target hosts, opts
|
|
162
198
|
storage = host.configManager.storageSystem
|
163
199
|
storage.UpdateSoftwareInternetScsiEnabled(:enabled => true)
|
164
200
|
adapter = storage.storageDeviceInfo.hostBusAdapter.grep(VIM::HostInternetScsiHba)[0]
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
201
|
+
if opts[:dynamic_target]
|
202
|
+
storage.UpdateInternetScsiName(
|
203
|
+
:iScsiHbaDevice => adapter.device,
|
204
|
+
:iScsiName => opts[:iqn]
|
205
|
+
)
|
206
|
+
storage.AddInternetScsiSendTargets(
|
207
|
+
:iScsiHbaDevice => adapter.device,
|
208
|
+
:targets => [
|
209
|
+
VIM::HostInternetScsiHbaSendTarget(:address => opts[:address])
|
210
|
+
]
|
211
|
+
)
|
212
|
+
else
|
213
|
+
storage.AddInternetScsiStaticTargets(
|
214
|
+
:iScsiHbaDevice => adapter.device,
|
215
|
+
:targets => [
|
216
|
+
VIM::HostInternetScsiHbaStaticTarget(
|
217
|
+
:address => opts[:address],
|
218
|
+
:iScsiName => opts[:iqn])
|
219
|
+
]
|
220
|
+
)
|
221
|
+
end
|
169
222
|
storage.RescanAllHba
|
170
223
|
end
|
171
224
|
end
|
@@ -203,3 +256,62 @@ def rescan_storage hosts
|
|
203
256
|
storageSystem.RescanVmfs
|
204
257
|
end
|
205
258
|
end
|
259
|
+
|
260
|
+
|
261
|
+
opts :select_vmknic_for_service do
|
262
|
+
summary "Selects a vmknic for a particular service"
|
263
|
+
arg :vmknic, "Name of vmknic", :type => :string
|
264
|
+
arg :service, "e.g.: vmotion", :type => :string
|
265
|
+
arg :host, nil, :lookup => VIM::HostSystem, :multi => true
|
266
|
+
end
|
267
|
+
|
268
|
+
def select_vmknic_for_service vmknic, service, hosts
|
269
|
+
hosts.each do |host|
|
270
|
+
vnicSys = host.configManager.virtualNicManager
|
271
|
+
vnicSys.SelectVnicForNicType(:nicType => service, :device => vmknic)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
opts :deselect_vmknic_for_service do
|
277
|
+
summary "Selects a vmknic for a particular service"
|
278
|
+
arg :vmknic, "Name of vmknic", :type => :string
|
279
|
+
arg :service, "e.g.: vmotion", :type => :string
|
280
|
+
arg :host, nil, :lookup => VIM::HostSystem, :multi => true
|
281
|
+
end
|
282
|
+
|
283
|
+
def deselect_vmknic_for_service vmknic, service, hosts
|
284
|
+
hosts.each do |host|
|
285
|
+
vnicSys = host.configManager.virtualNicManager
|
286
|
+
vnicSys.DeselectVnicForNicType(:nicType => service, :device => vmknic)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library
|
291
|
+
def ssh_exec!(ssh, command)
|
292
|
+
stdout_data = ""
|
293
|
+
stderr_data = ""
|
294
|
+
exit_code = nil
|
295
|
+
exit_signal = nil
|
296
|
+
ssh.open_channel do |channel|
|
297
|
+
channel.exec(command) do |ch, success|
|
298
|
+
unless success
|
299
|
+
abort "FAILED: couldn't execute command (ssh.channel.exec)"
|
300
|
+
end
|
301
|
+
channel.on_data do |ch,data|
|
302
|
+
stdout_data+=data
|
303
|
+
end
|
304
|
+
|
305
|
+
channel.on_extended_data do |ch,type,data|
|
306
|
+
stderr_data+=data
|
307
|
+
end
|
308
|
+
|
309
|
+
channel.on_request("exit-status") do |ch,data|
|
310
|
+
exit_code = data.read_long
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
end
|
315
|
+
ssh.loop
|
316
|
+
[stdout_data, stderr_data, exit_code]
|
317
|
+
end
|
data/lib/rvc/modules/perf.rb
CHANGED
@@ -244,14 +244,16 @@ def counters obj
|
|
244
244
|
groups = available_counters.group_by { |counter| counter.groupInfo }
|
245
245
|
|
246
246
|
table = Terminal::Table.new
|
247
|
-
table.add_row ["Name", "Description", "Unit", "Level", "Active Intervals"]
|
247
|
+
table.add_row ["Name", "Description", "Unit", "Level", "Per-Dev", "Active Intervals"]
|
248
248
|
groups.sort_by { |group,counters| group.key }.each do |group,counters|
|
249
249
|
table.add_separator
|
250
250
|
table.add_row [{ :value => group.label, :colspan => 5}]
|
251
251
|
table.add_separator
|
252
252
|
counters.sort_by(&:name).each do |counter|
|
253
|
+
pp counter
|
253
254
|
table.add_row [counter.name, counter.nameInfo.label, counter.unitInfo.label,
|
254
|
-
counter.level,
|
255
|
+
counter.level, counter.perDeviceLevel,
|
256
|
+
active_intervals_text[counter.level]]
|
255
257
|
end
|
256
258
|
end
|
257
259
|
puts(table)
|
@@ -305,11 +307,41 @@ def counter counter_name, obj
|
|
305
307
|
end
|
306
308
|
end
|
307
309
|
|
310
|
+
|
311
|
+
opts :modify_counters do
|
312
|
+
summary "Modify perf counter settings"
|
313
|
+
arg :metrics, nil, :type => :string, :multi => true
|
314
|
+
opt :aggregate_level, nil, :type => :int
|
315
|
+
opt :per_device_level, nil, :type => :int
|
316
|
+
end
|
317
|
+
|
318
|
+
def modify_counters counter_names, opts
|
319
|
+
vim = lookup_single('~@')
|
320
|
+
pm = vim.serviceContent.perfManager
|
321
|
+
pm.UpdateCounterLevelMapping(
|
322
|
+
:counterLevelMap => counter_names.map do |counter_name|
|
323
|
+
counter = pm.perfcounter_hash[counter_name]
|
324
|
+
if !counter
|
325
|
+
err "no such counter #{counter_name.inspect}"
|
326
|
+
end
|
327
|
+
|
328
|
+
aggregateLevel = opts[:aggregate_level] || counter.level
|
329
|
+
perDeviceLevel = opts[:per_device_level] || counter.perDeviceLevel
|
330
|
+
{
|
331
|
+
:counterId => counter.key,
|
332
|
+
:aggregateLevel => aggregateLevel,
|
333
|
+
:perDeviceLevel => perDeviceLevel,
|
334
|
+
}
|
335
|
+
end
|
336
|
+
)
|
337
|
+
end
|
338
|
+
|
308
339
|
opts :stats do
|
309
340
|
summary "Retrieve performance stats for given object"
|
310
341
|
arg :metrics, nil, :type => :string
|
311
342
|
arg :obj, nil, :multi => true, :lookup => VIM::ManagedEntity
|
312
343
|
opt :samples, "Number of samples to retrieve", :type => :int
|
344
|
+
opt :instance, "Specify a particular insance instead of *", :type => :string, :multi => true
|
313
345
|
end
|
314
346
|
|
315
347
|
def stats metrics, objs, opts
|
@@ -317,6 +349,7 @@ def stats metrics, objs, opts
|
|
317
349
|
|
318
350
|
vim = single_connection objs
|
319
351
|
pm = vim.serviceContent.perfManager
|
352
|
+
pc = vim.propertyCollector
|
320
353
|
|
321
354
|
metrics.each do |x|
|
322
355
|
err "no such metric #{x}" unless pm.perfcounter_hash.member? x
|
@@ -329,21 +362,34 @@ def stats metrics, objs, opts
|
|
329
362
|
interval = 300
|
330
363
|
start_time = Time.now - 300 * 5
|
331
364
|
end
|
365
|
+
instances = opts[:instance] || ['*']
|
366
|
+
if instances.length == 0
|
367
|
+
instances << '*'
|
368
|
+
end
|
332
369
|
stat_opts = {
|
333
370
|
:interval => interval,
|
334
371
|
:startTime => start_time,
|
372
|
+
:instance => instances,
|
373
|
+
:multi_instance => true,
|
335
374
|
}
|
336
375
|
stat_opts[:max_samples] = opts[:samples] if opts[:samples]
|
337
376
|
res = pm.retrieve_stats objs, metrics, stat_opts
|
338
377
|
|
339
378
|
table = Terminal::Table.new
|
340
|
-
table.add_row ['Object', 'Metric', 'Values', 'Unit']
|
379
|
+
table.add_row ['Object', 'Instance', 'Metric', 'Values', 'Unit']
|
341
380
|
table.add_separator
|
342
|
-
objs
|
381
|
+
objs_props = pc.collectMultiple(objs, 'name')
|
382
|
+
objs.sort_by{|obj| objs_props[obj]['name']}.each do |obj|
|
383
|
+
if !res[obj]
|
384
|
+
next
|
385
|
+
end
|
343
386
|
metrics.each do |metric|
|
344
|
-
|
345
|
-
|
346
|
-
|
387
|
+
matches = res[obj][:metrics].select{|k, v| k[0] == metric}
|
388
|
+
matches.each do |key, stat|
|
389
|
+
key, instance = key
|
390
|
+
metric_info = pm.perfcounter_hash[metric]
|
391
|
+
table.add_row([objs_props[obj]['name'], instance, metric, stat.join(','), metric_info.unitInfo.label])
|
392
|
+
end
|
347
393
|
end
|
348
394
|
end
|
349
395
|
puts table
|
data/lib/rvc/modules/snapshot.rb
CHANGED
@@ -77,6 +77,7 @@ opts :remove do
|
|
77
77
|
summary "Remove snapshots"
|
78
78
|
arg :snapshots, nil, :multi => true, :lookup => RVC::SnapshotFolder
|
79
79
|
opt :remove_children, "Whether to remove the snapshot's children too"
|
80
|
+
opt :no_consolidate, "Don't consolidate", :type => :boolean
|
80
81
|
end
|
81
82
|
|
82
83
|
def remove snapshots, opts
|
@@ -84,6 +85,11 @@ def remove snapshots, opts
|
|
84
85
|
snapshots.sort_by! {|s| s.rvc_path_str }
|
85
86
|
|
86
87
|
snapshots.reverse_each do |snapshot|
|
87
|
-
tasks
|
88
|
+
tasks(
|
89
|
+
[snapshot.find_tree.snapshot],
|
90
|
+
:RemoveSnapshot,
|
91
|
+
:removeChildren => opts[:remove_children],
|
92
|
+
:consolidate => !opts[:no_consolidate]
|
93
|
+
)
|
88
94
|
end
|
89
95
|
end
|
@@ -0,0 +1,728 @@
|
|
1
|
+
# Copyright (c) 2013 VMware, Inc. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rbvmomi/vim'
|
22
|
+
require 'rbvmomi/pbm'
|
23
|
+
require 'rvc/vim'
|
24
|
+
|
25
|
+
PBM = RbVmomi::PBM
|
26
|
+
|
27
|
+
class RbVmomi::VIM
|
28
|
+
def pbm
|
29
|
+
@pbm ||= PBM.connect self, :insecure => true
|
30
|
+
end
|
31
|
+
|
32
|
+
def pbm= x
|
33
|
+
@pbm = nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module PbmHelperModule
|
38
|
+
def _catch_spbm_resets(conn)
|
39
|
+
begin
|
40
|
+
yield
|
41
|
+
rescue EOFError
|
42
|
+
if conn
|
43
|
+
conn.pbm = nil
|
44
|
+
end
|
45
|
+
raise "Connection to SPBM timed out, try again"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
RbVmomi::VIM::Datacenter
|
51
|
+
class RbVmomi::VIM::Datacenter
|
52
|
+
def rvc_list_children_profiles
|
53
|
+
{
|
54
|
+
'storage' => RVC::FakeFolder.new(self, :rvc_children_storage),
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def rvc_children_storage
|
59
|
+
{
|
60
|
+
'vmprofiles' => RVC::FakeFolder.new(self, :rvc_children_profiles),
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def rvc_children_profiles
|
65
|
+
conn = _connection
|
66
|
+
_catch_spbm_resets(conn) do
|
67
|
+
pbm = conn.pbm
|
68
|
+
pm = pbm.serviceContent.profileManager
|
69
|
+
profileIds = pm.PbmQueryProfile(
|
70
|
+
:resourceType => {:resourceType => "STORAGE"},
|
71
|
+
:profileCategory => "REQUIREMENT"
|
72
|
+
)
|
73
|
+
if profileIds.length > 0
|
74
|
+
profiles = pm.PbmRetrieveContent(:profileIds => profileIds)
|
75
|
+
else
|
76
|
+
profiles = []
|
77
|
+
end
|
78
|
+
|
79
|
+
Hash[profiles.map do |x|
|
80
|
+
x.instance_variable_set(:@connection, pbm)
|
81
|
+
x.instance_variable_set(:@dc, self)
|
82
|
+
[x.name, x]
|
83
|
+
end]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
RbVmomi::PBM::PbmCapabilityInstance
|
89
|
+
class RbVmomi::PBM::PbmCapabilityInstance
|
90
|
+
def name
|
91
|
+
"#{self.id.namespace}.#{self.id.id}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
RbVmomi::PBM::PbmCapabilityMetadata
|
97
|
+
class RbVmomi::PBM::PbmCapabilityMetadata
|
98
|
+
def name
|
99
|
+
"#{self.id.namespace}.#{self.id.id}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
RbVmomi::VIM::VirtualDisk
|
104
|
+
class RbVmomi::VIM::VirtualDisk
|
105
|
+
def rvc_display_info_vsan
|
106
|
+
if self.backing.backingObjectId
|
107
|
+
puts "VSAN objects:"
|
108
|
+
backing = self.backing
|
109
|
+
while backing
|
110
|
+
puts " #{backing.backingObjectId}"
|
111
|
+
backing = backing.parent
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
RbVmomi::VIM::Datastore
|
119
|
+
class RbVmomi::VIM::Datastore
|
120
|
+
def to_pbmhub
|
121
|
+
PBM::PbmPlacementHub(:hubType => "Datastore", :hubId => _ref)
|
122
|
+
end
|
123
|
+
|
124
|
+
def pbm_capability_profiles
|
125
|
+
pbm_associated_profiles
|
126
|
+
end
|
127
|
+
|
128
|
+
def rvc_list_children_capabilitysets
|
129
|
+
{
|
130
|
+
'capabilitysets' => RVC::FakeFolder.new(self, :rvc_children_capabilitysets),
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def rvc_children_capabilitysets
|
135
|
+
conn = _connection
|
136
|
+
_catch_spbm_resets(conn) do
|
137
|
+
pbm = _connection.pbm
|
138
|
+
profiles = pbm_capability_profiles
|
139
|
+
Hash[profiles.map do |x|
|
140
|
+
x.instance_variable_set(:@connection, pbm)
|
141
|
+
x.instance_variable_set(:@dc, self)
|
142
|
+
[x.name, x]
|
143
|
+
end]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
RbVmomi::VIM::VirtualMachine
|
149
|
+
class RbVmomi::VIM::VirtualMachine
|
150
|
+
def rvc_list_children_vmprofiles
|
151
|
+
{
|
152
|
+
'vmprofiles' => RVC::FakeFolder.new(self, :rvc_children_vmprofiles),
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
def rvc_children_vmprofiles
|
157
|
+
conn = _connection
|
158
|
+
_catch_spbm_resets(conn) do
|
159
|
+
pbm = _connection.pbm
|
160
|
+
profiles = pbm_associated_profiles
|
161
|
+
Hash[profiles.map do |x|
|
162
|
+
x.instance_variable_set(:@connection, pbm)
|
163
|
+
x.instance_variable_set(:@dc, self)
|
164
|
+
[x.name, x]
|
165
|
+
end]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
RbVmomi::VIM::ManagedObject
|
171
|
+
class RbVmomi::VIM::ManagedObject
|
172
|
+
def to_pbmobjref
|
173
|
+
type = self.class.wsdl_name
|
174
|
+
type = "%s%s" % [type[0].downcase, type[1..-1]]
|
175
|
+
PBM::PbmServerObjectRef(
|
176
|
+
:objectType => type,
|
177
|
+
:key => _ref,
|
178
|
+
:serverUuid => _connection.serviceContent.about.instanceUuid
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
def pbm_associated_profiles
|
183
|
+
conn = _connection
|
184
|
+
_catch_spbm_resets(conn) do
|
185
|
+
pbm = _connection.pbm
|
186
|
+
pm = pbm.serviceContent.profileManager
|
187
|
+
ids = pm.PbmQueryAssociatedProfile(:entity => self.to_pbmobjref)
|
188
|
+
pm.PbmRetrieveContent(:profileIds => ids)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def _catch_spbm_resets(conn)
|
193
|
+
begin
|
194
|
+
yield
|
195
|
+
rescue EOFError
|
196
|
+
if conn
|
197
|
+
conn.pbm = nil
|
198
|
+
end
|
199
|
+
raise "Connection to SPBM timed out, try again"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
RbVmomi::VIM::VirtualMachine
|
205
|
+
class RbVmomi::VIM::VirtualMachine
|
206
|
+
def disks_pbmobjref(opts = {})
|
207
|
+
if opts[:vms_props] && opts[:vms_props][self] &&
|
208
|
+
opts[:vms_props][self]['config.hardware.device']
|
209
|
+
devices = opts[:vms_props][self]['config.hardware.device']
|
210
|
+
_disks = devices.select{|x| x.is_a?(VIM::VirtualDisk)}
|
211
|
+
else
|
212
|
+
_disks = disks
|
213
|
+
end
|
214
|
+
_disks.map do |disk|
|
215
|
+
PBM::PbmServerObjectRef(
|
216
|
+
:objectType => "virtualDiskId",
|
217
|
+
:key => "#{self._ref}:#{disk.key}",
|
218
|
+
:serverUuid => _connection.serviceContent.about.instanceUuid
|
219
|
+
)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def all_pbmobjref(opts = {})
|
224
|
+
[to_pbmobjref] + disks_pbmobjref(opts)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
RbVmomi::PBM::PbmPlacementSolver
|
229
|
+
class RbVmomi::PBM::PbmPlacementSolver
|
230
|
+
def find_compatible_datastores datastores, profileIds
|
231
|
+
if profileIds.length > 1
|
232
|
+
raise Exception("Passing in more than one profile currently not supported")
|
233
|
+
end
|
234
|
+
dsMoMap = Hash[datastores.map{|x| [x._ref, x]}]
|
235
|
+
results = self.PbmSolve(
|
236
|
+
:hubsToSearch => datastores.map{|x| x.to_pbmhub},
|
237
|
+
:requirements => [
|
238
|
+
{
|
239
|
+
:subject => PBM.PbmPlacementSubject(
|
240
|
+
:subjectType=>"VirtualMachine",
|
241
|
+
:subjectId=>"fake"
|
242
|
+
),
|
243
|
+
:requirement => [
|
244
|
+
PBM::PbmPlacementCapabilityProfileRequirement(
|
245
|
+
:requirementType => "type",
|
246
|
+
:mandatory => true,
|
247
|
+
:profileId => profileIds[0]
|
248
|
+
)
|
249
|
+
],
|
250
|
+
}
|
251
|
+
],
|
252
|
+
:partialSolution => false
|
253
|
+
)
|
254
|
+
compatibleDsList = results.map do |x|
|
255
|
+
dsMoMap[x.subjectAssignment[0].hub.hubId]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
RbVmomi::PBM::PbmCapabilityProfile
|
261
|
+
class RbVmomi::PBM::PbmCapabilityProfile
|
262
|
+
include InventoryObject
|
263
|
+
include PbmHelperModule
|
264
|
+
|
265
|
+
def children
|
266
|
+
{
|
267
|
+
'datastores' => RVC::FakeFolder.new(self, :rvc_children_datastores),
|
268
|
+
'vms' => RVC::FakeFolder.new(self, :rvc_children_vms),
|
269
|
+
}
|
270
|
+
end
|
271
|
+
|
272
|
+
def rvc_children_vms
|
273
|
+
pbm = @connection
|
274
|
+
vim = @dc._connection
|
275
|
+
pc = vim.propertyCollector
|
276
|
+
pm = pbm.serviceContent.profileManager
|
277
|
+
|
278
|
+
vms = pm.PbmQueryAssociatedEntity(
|
279
|
+
:profile => self.profileId,
|
280
|
+
:entityType => 'virtualMachine'
|
281
|
+
)
|
282
|
+
vms = vms.map do |ref|
|
283
|
+
VIM::VirtualMachine(vim, ref.key)
|
284
|
+
end
|
285
|
+
props = pc.collectMultiple(vms, 'name')
|
286
|
+
Hash[props.map do |vm, vm_props|
|
287
|
+
[vm_props['name'], vm]
|
288
|
+
end]
|
289
|
+
end
|
290
|
+
|
291
|
+
def rvc_children_datastores
|
292
|
+
pbm = @connection
|
293
|
+
vim = @dc._connection
|
294
|
+
pc = vim.propertyCollector
|
295
|
+
_catch_spbm_resets(vim) do
|
296
|
+
solver = pbm.serviceContent.placementSolver
|
297
|
+
datastores = solver.find_compatible_datastores @dc.datastore, [profileId]
|
298
|
+
props = pc.collectMultiple(datastores, 'name')
|
299
|
+
Hash[props.map do |ds, ds_props|
|
300
|
+
[ds_props['name'], ds]
|
301
|
+
end]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def display_info
|
306
|
+
super
|
307
|
+
puts "Name: #{name}"
|
308
|
+
puts "Description:"
|
309
|
+
puts description
|
310
|
+
puts "ProfileId: #{profileId.uniqueId}"
|
311
|
+
puts "Type: #{resourceType.resourceType} - #{profileCategory}"
|
312
|
+
puts "Rule-Sets:"
|
313
|
+
constraints.subProfiles.each_with_index do |sub, i|
|
314
|
+
puts " Rule-Set ##{i + 1}:"
|
315
|
+
sub.capability.each do |rule|
|
316
|
+
instances = rule.constraint.map{|c| c.propertyInstance}.flatten
|
317
|
+
if instances.length > 1
|
318
|
+
raise "Can't deal with multiple constraints in single rule"
|
319
|
+
end
|
320
|
+
value = instances[0].value
|
321
|
+
if value.is_a?(RbVmomi::PBM::PbmCapabilityRange)
|
322
|
+
value = "#{value.min} - #{value.max}"
|
323
|
+
end
|
324
|
+
puts " #{rule.name}: #{value}"
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
|
331
|
+
opts :profile_delete do
|
332
|
+
summary "Delete a VM Storage Profile"
|
333
|
+
arg :profile, nil, :lookup => RbVmomi::PBM::PbmCapabilityProfile, :multi => true
|
334
|
+
end
|
335
|
+
|
336
|
+
def profile_delete profiles
|
337
|
+
if profiles.length == 0
|
338
|
+
return
|
339
|
+
end
|
340
|
+
_catch_spbm_resets(nil) do
|
341
|
+
pbm = profiles.first.instance_variable_get(:@connection)
|
342
|
+
pm = pbm.serviceContent.profileManager
|
343
|
+
pm.PbmDelete(:profileId => profiles.map{|x| x.profileId})
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
|
348
|
+
opts :profile_apply do
|
349
|
+
summary "Apply a VM Storage Profile. Pushed profile content to Storage system"
|
350
|
+
arg :profile, nil, :lookup => RbVmomi::PBM::PbmCapabilityProfile, :multi => true
|
351
|
+
end
|
352
|
+
|
353
|
+
def profile_apply profiles
|
354
|
+
if profiles.length == 0
|
355
|
+
return
|
356
|
+
end
|
357
|
+
pbm = profiles.first.instance_variable_get(:@connection)
|
358
|
+
dc = profiles.first.instance_variable_get(:@dc)
|
359
|
+
vim = dc._connection
|
360
|
+
_catch_spbm_resets(vim) do
|
361
|
+
pm = pbm.serviceContent.profileManager
|
362
|
+
results = pm.applyProfile(:profiles => profiles.map{|x| x.profileId})
|
363
|
+
tasks = results.map{|x| x.reconfigOutcome.map{|y| y.taskMoid}}.flatten
|
364
|
+
tasks = tasks.map{|x| VIM::Task(vim, x)}
|
365
|
+
progress(tasks)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
|
370
|
+
opts :profile_create do
|
371
|
+
summary "Create a VM Storage Profile"
|
372
|
+
arg :name, nil, :type => :string
|
373
|
+
opt :description, "Description", :type => :string
|
374
|
+
opt :rule, "Rule in format <provider>.<capability>=<value>", :type => :string, :multi => true
|
375
|
+
end
|
376
|
+
|
377
|
+
def profile_create profile_name, opts
|
378
|
+
dc, = lookup '~'
|
379
|
+
conn = dc._connection
|
380
|
+
_catch_spbm_resets(conn) do
|
381
|
+
pbm = conn.pbm
|
382
|
+
pm = pbm.serviceContent.profileManager
|
383
|
+
|
384
|
+
rules = opts[:rule] || []
|
385
|
+
resType = {:resourceType => "STORAGE"}
|
386
|
+
|
387
|
+
# Need to support other vendors too
|
388
|
+
cm = pm.PbmFetchCapabilityMetadata(
|
389
|
+
:resourceType => resType,
|
390
|
+
:vendorUuid => "com.vmware.storage.vsan"
|
391
|
+
)
|
392
|
+
capabilities = cm.map{|x| x.capabilityMetadata}.flatten
|
393
|
+
|
394
|
+
constraints = rules.map do |rule_str|
|
395
|
+
name, values_str = rule_str.split("=", 2)
|
396
|
+
if !values_str
|
397
|
+
err "Rule is malformed: #{rule_str}, should be <provider>.<capability>=<value>"
|
398
|
+
end
|
399
|
+
ns, id = name.split('.', 2)
|
400
|
+
if !id
|
401
|
+
err "Rule is malformed: #{rule_str}, should be <provider>.<capability>=<value>"
|
402
|
+
end
|
403
|
+
capability = capabilities.find{|x| x.name == name}
|
404
|
+
if !capability
|
405
|
+
err "Capability #{name} unknown"
|
406
|
+
end
|
407
|
+
type = capability.propertyMetadata[0].type
|
408
|
+
values = values_str.split(',')
|
409
|
+
if type.typeName == "XSD_INT"
|
410
|
+
values = values.map{|x| RbVmomi::BasicTypes::Int.new(x.to_i)}
|
411
|
+
end
|
412
|
+
if type.typeName == "XSD_BOOLEAN"
|
413
|
+
values = values.map{|x| (x =~ /(true|True|1|yes|Yes)/) != nil}
|
414
|
+
end
|
415
|
+
if type.is_a?(PBM::PbmCapabilityGenericTypeInfo) && type.genericTypeName == "VMW_RANGE"
|
416
|
+
if values.length != 2
|
417
|
+
err "#{name} is a range, need to specify 2 values"
|
418
|
+
end
|
419
|
+
value = PBM::PbmCapabilityTypesRange(:min => values[0], :max => values[1])
|
420
|
+
elsif values.length == 1
|
421
|
+
value = values.first
|
422
|
+
else
|
423
|
+
err "Value malformed: #{value_str}"
|
424
|
+
end
|
425
|
+
|
426
|
+
{
|
427
|
+
:id => {
|
428
|
+
:namespace => ns,
|
429
|
+
:id => id
|
430
|
+
},
|
431
|
+
:constraint => [{
|
432
|
+
:propertyInstance => [{
|
433
|
+
:id => id,
|
434
|
+
:value => value
|
435
|
+
}]
|
436
|
+
}]
|
437
|
+
}
|
438
|
+
end
|
439
|
+
pm.PbmCreate(
|
440
|
+
:createSpec => {
|
441
|
+
:name => profile_name,
|
442
|
+
:description => opts[:description],
|
443
|
+
:resourceType => resType,
|
444
|
+
:constraints => PBM::PbmCapabilitySubProfileConstraints(
|
445
|
+
:subProfiles => [
|
446
|
+
PBM::PbmCapabilitySubProfile(
|
447
|
+
:name => "Object",
|
448
|
+
:capability => constraints
|
449
|
+
)
|
450
|
+
]
|
451
|
+
)
|
452
|
+
}
|
453
|
+
)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
opts :device_change_storage_profile do
|
458
|
+
summary "Change storage profile of a virtual disk"
|
459
|
+
arg :device, nil, :lookup => VIM::VirtualDevice, :multi => true
|
460
|
+
opt :profile, "Profile", :lookup => RbVmomi::PBM::PbmCapabilityProfile
|
461
|
+
end
|
462
|
+
|
463
|
+
def device_change_storage_profile devs, opts
|
464
|
+
if !opts[:profile]
|
465
|
+
err "Must specify a storage profile"
|
466
|
+
end
|
467
|
+
|
468
|
+
vm_devs = devs.group_by(&:rvc_vm)
|
469
|
+
conn = vm_devs.keys.first._connection
|
470
|
+
_catch_spbm_resets(conn) do
|
471
|
+
_run_with_rev(conn, "dev") do
|
472
|
+
profile = nil
|
473
|
+
if opts[:profile]
|
474
|
+
profile = [VIM::VirtualMachineDefinedProfileSpec(
|
475
|
+
:profileId => opts[:profile].profileId.uniqueId
|
476
|
+
)]
|
477
|
+
end
|
478
|
+
tasks = vm_devs.map do |vm, my_devs|
|
479
|
+
spec = {
|
480
|
+
:deviceChange => my_devs.map do |dev|
|
481
|
+
{
|
482
|
+
:operation => :edit,
|
483
|
+
:device => dev,
|
484
|
+
:profile => profile,
|
485
|
+
}
|
486
|
+
end
|
487
|
+
}
|
488
|
+
vm.ReconfigVM_Task(:spec => spec)
|
489
|
+
end
|
490
|
+
progress(tasks)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
opts :check_compliance do
|
496
|
+
summary "Check compliance"
|
497
|
+
arg :vm, nil, :lookup => VIM::VirtualMachine, :multi => true
|
498
|
+
end
|
499
|
+
|
500
|
+
def check_compliance vms
|
501
|
+
dc, = lookup '~'
|
502
|
+
conn = dc._connection
|
503
|
+
_catch_spbm_resets(conn) do
|
504
|
+
pbm = conn.pbm
|
505
|
+
pm = pbm.serviceContent.profileManager
|
506
|
+
cm = pbm.serviceContent.complianceManager
|
507
|
+
|
508
|
+
compliance = cm.PbmCheckCompliance(:entities => vms.map do |vm|
|
509
|
+
vm.all_pbmobjref
|
510
|
+
end.flatten)
|
511
|
+
profile_ids = Hash[compliance.map{|x| [x.entity.key, x.profile.uniqueId]}]
|
512
|
+
compliances = Hash[compliance.map{|x| [x.entity.key, x.complianceStatus]}]
|
513
|
+
|
514
|
+
profiles = nil
|
515
|
+
begin
|
516
|
+
profileIds = profile_ids.values.uniq.compact.map do |x|
|
517
|
+
PBM::PbmProfileId(:uniqueId => x)
|
518
|
+
end
|
519
|
+
if profileIds.length > 0
|
520
|
+
profiles = pm.PbmRetrieveContent(
|
521
|
+
:profileIds => profileIds
|
522
|
+
)
|
523
|
+
else
|
524
|
+
profiles = []
|
525
|
+
end
|
526
|
+
rescue Exception => ex
|
527
|
+
pp "#{ex.class}: #{ex.message}"
|
528
|
+
pp profile_ids
|
529
|
+
raise ex
|
530
|
+
end
|
531
|
+
profiles = Hash[profiles.map{|x| [x.profileId.uniqueId, x.name]}]
|
532
|
+
profiles = Hash[profile_ids.map{|k,v| [k, profiles[v] || v]}]
|
533
|
+
|
534
|
+
t = Terminal::Table.new()
|
535
|
+
t << ['VM/Virtual Disk', 'Profile', 'Compliance']
|
536
|
+
t.add_separator
|
537
|
+
vms.each do |vm|
|
538
|
+
t << [
|
539
|
+
vm.name,
|
540
|
+
profiles[vm._ref] || "unknown",
|
541
|
+
compliances[vm._ref] || "unknown",
|
542
|
+
]
|
543
|
+
vm.disks.each do |disk|
|
544
|
+
id = "#{vm._ref}:#{disk.key}"
|
545
|
+
t << [
|
546
|
+
" #{disk.deviceInfo.label}",
|
547
|
+
profiles[id] || "unknown",
|
548
|
+
compliances[id] || "unknown",
|
549
|
+
]
|
550
|
+
end
|
551
|
+
end
|
552
|
+
puts t
|
553
|
+
|
554
|
+
puts ""
|
555
|
+
stats = Hash[compliances.values.group_by{|x| x}.map{|k,v| [k, v.length]}]
|
556
|
+
stats.sort_by{|k,v| k}.each do |type, count|
|
557
|
+
puts "Number of '#{type}' entities: #{count}"
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
opts :namespace_change_storage_profile do
|
563
|
+
summary "Change storage profile of VM namespace"
|
564
|
+
arg :vm, nil, :lookup => VIM::VirtualMachine, :multi => true
|
565
|
+
opt :profile, "Profile", :lookup => RbVmomi::PBM::PbmCapabilityProfile
|
566
|
+
end
|
567
|
+
|
568
|
+
def namespace_change_storage_profile vms, opts
|
569
|
+
if !opts[:profile]
|
570
|
+
err "Must specify a storage profile"
|
571
|
+
end
|
572
|
+
|
573
|
+
conn = vms.first._connection
|
574
|
+
_catch_spbm_resets(conn) do
|
575
|
+
_run_with_rev(conn, "dev") do
|
576
|
+
profile = nil
|
577
|
+
if opts[:profile]
|
578
|
+
profile = [VIM::VirtualMachineDefinedProfileSpec(
|
579
|
+
:profileId => opts[:profile].profileId.uniqueId
|
580
|
+
)]
|
581
|
+
end
|
582
|
+
tasks = vms.map do |vm|
|
583
|
+
spec = {
|
584
|
+
:vmProfile => profile,
|
585
|
+
}
|
586
|
+
vm.ReconfigVM_Task(:spec => spec)
|
587
|
+
end
|
588
|
+
progress(tasks)
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
opts :vm_change_storage_profile do
|
594
|
+
summary "Change storage profile of VM namespace and its disks"
|
595
|
+
arg :vm, nil, :lookup => VIM::VirtualMachine, :multi => true
|
596
|
+
opt :profile, "Profile", :lookup => RbVmomi::PBM::PbmCapabilityProfile
|
597
|
+
end
|
598
|
+
|
599
|
+
def vm_change_storage_profile vms, opts
|
600
|
+
if !opts[:profile]
|
601
|
+
err "Must specify a storage profile"
|
602
|
+
end
|
603
|
+
|
604
|
+
conn = vms.first._connection
|
605
|
+
_catch_spbm_resets(conn) do
|
606
|
+
_run_with_rev(conn, "dev") do
|
607
|
+
profile = nil
|
608
|
+
if opts[:profile]
|
609
|
+
profile = [VIM::VirtualMachineDefinedProfileSpec(
|
610
|
+
:profileId => opts[:profile].profileId.uniqueId
|
611
|
+
)]
|
612
|
+
end
|
613
|
+
tasks = vms.map do |vm|
|
614
|
+
disks = vm.disks
|
615
|
+
spec = {
|
616
|
+
:vmProfile => profile,
|
617
|
+
:deviceChange => disks.map do |dev|
|
618
|
+
{
|
619
|
+
:operation => :edit,
|
620
|
+
:device => dev,
|
621
|
+
:profile => profile,
|
622
|
+
}
|
623
|
+
end
|
624
|
+
}
|
625
|
+
vm.ReconfigVM_Task(:spec => spec)
|
626
|
+
end
|
627
|
+
progress(tasks)
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
opts :device_add_disk do
|
633
|
+
summary "Add a hard drive to a virtual machine"
|
634
|
+
arg :vm, nil, :lookup => VIM::VirtualMachine
|
635
|
+
arg :path, "Filename on the datastore", :lookup_parent => VIM::Datastore::FakeDatastoreFolder, :required => false
|
636
|
+
opt :size, 'Size', :default => '10G'
|
637
|
+
opt :controller, 'Virtual controller', :type => :string, :lookup => VIM::VirtualController
|
638
|
+
opt :file_op, 'File operation (create|reuse|replace)', :default => 'create'
|
639
|
+
opt :profile, "Profile", :lookup => RbVmomi::PBM::PbmCapabilityProfile
|
640
|
+
end
|
641
|
+
|
642
|
+
def device_add_disk vm, path, opts
|
643
|
+
controller, unit_number = pick_controller vm, opts[:controller], [VIM::VirtualSCSIController, VIM::VirtualIDEController]
|
644
|
+
id = "disk-#{controller.key}-#{unit_number}"
|
645
|
+
|
646
|
+
if path
|
647
|
+
dir, file = *path
|
648
|
+
filename = "#{dir.datastore_path}/#{file}"
|
649
|
+
else
|
650
|
+
filename = "#{File.dirname(vm.summary.config.vmPathName)}/#{id}.vmdk"
|
651
|
+
end
|
652
|
+
|
653
|
+
opts[:file_op] = nil if opts[:file_op] == 'reuse'
|
654
|
+
|
655
|
+
conn = vm._connection
|
656
|
+
_run_with_rev(conn, "dev") do
|
657
|
+
profile = nil
|
658
|
+
if opts[:profile]
|
659
|
+
profile = [VIM::VirtualMachineDefinedProfileSpec(
|
660
|
+
:profileId => opts[:profile].profileId.uniqueId
|
661
|
+
)]
|
662
|
+
end
|
663
|
+
spec = {
|
664
|
+
:deviceChange => [
|
665
|
+
{
|
666
|
+
:operation => :add,
|
667
|
+
:fileOperation => opts[:file_op],
|
668
|
+
:device => VIM::VirtualDisk(
|
669
|
+
:key => -1,
|
670
|
+
:backing => VIM.VirtualDiskFlatVer2BackingInfo(
|
671
|
+
:fileName => filename,
|
672
|
+
:diskMode => :persistent,
|
673
|
+
:thinProvisioned => true
|
674
|
+
),
|
675
|
+
:capacityInKB => MetricNumber.parse(opts[:size]).to_i/1000,
|
676
|
+
:controllerKey => controller.key,
|
677
|
+
:unitNumber => unit_number
|
678
|
+
),
|
679
|
+
:profile => profile,
|
680
|
+
},
|
681
|
+
]
|
682
|
+
}
|
683
|
+
task = vm.ReconfigVM_Task(:spec => spec)
|
684
|
+
result = progress([task])[task]
|
685
|
+
if result == nil
|
686
|
+
new_device = vm.collect('config.hardware.device')[0].grep(VIM::VirtualDisk).last
|
687
|
+
puts "Added device #{new_device.name}"
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
|
693
|
+
def pick_controller vm, controller, controller_classes
|
694
|
+
existing_devices, = vm.collect 'config.hardware.device'
|
695
|
+
|
696
|
+
controller ||= existing_devices.find do |dev|
|
697
|
+
controller_classes.any? { |klass| dev.is_a? klass } &&
|
698
|
+
dev.device.length < 2
|
699
|
+
end
|
700
|
+
err "no suitable controller found" unless controller
|
701
|
+
|
702
|
+
used_unit_numbers = existing_devices.select { |dev| dev.controllerKey == controller.key }.map(&:unitNumber)
|
703
|
+
unit_number = (used_unit_numbers.max||-1) + 1
|
704
|
+
|
705
|
+
[controller, unit_number]
|
706
|
+
end
|
707
|
+
|
708
|
+
|
709
|
+
def _run_with_rev conn, rev
|
710
|
+
old_rev = conn.rev
|
711
|
+
begin
|
712
|
+
conn.rev = rev
|
713
|
+
yield
|
714
|
+
ensure
|
715
|
+
conn.rev = old_rev
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
def _catch_spbm_resets(conn)
|
720
|
+
begin
|
721
|
+
yield
|
722
|
+
rescue EOFError
|
723
|
+
if conn
|
724
|
+
conn.pbm = nil
|
725
|
+
end
|
726
|
+
err "Connection to SPBM timed out, try again"
|
727
|
+
end
|
728
|
+
end
|