rvc 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|