rbvmomi 1.0.1
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/.gitignore +4 -0
- data/LICENSE +19 -0
- data/README.md +12 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/devel/analyze-vim-declarations.rb +206 -0
- data/devel/analyze-xml.rb +46 -0
- data/lib/rbvmomi.rb +283 -0
- data/lib/rbvmomi/extensions.rb +491 -0
- data/lib/rbvmomi/profile.rb +22 -0
- data/lib/rbvmomi/trollop.rb +29 -0
- data/lib/rbvmomi/types.rb +373 -0
- data/lib/trivial_soap.rb +83 -0
- data/test/runner.rb +3 -0
- data/test/test.rb +69 -0
- data/test/test_deserialization.rb +324 -0
- data/test/test_emit_request.rb +110 -0
- data/test/test_parse_response.rb +67 -0
- data/test/test_serialization.rb +208 -0
- data/vmodl.tc +0 -0
- metadata +151 -0
@@ -0,0 +1,491 @@
|
|
1
|
+
# Copyright (c) 2010 VMware, Inc. All Rights Reserved.
|
2
|
+
CURLBIN = ENV['CURL'] || "curl"
|
3
|
+
|
4
|
+
module RbVmomi::VIM
|
5
|
+
|
6
|
+
class ManagedObject
|
7
|
+
def wait version, *pathSet
|
8
|
+
version ||= ''
|
9
|
+
all = pathSet.empty?
|
10
|
+
filter = @soap.propertyCollector.CreateFilter :spec => {
|
11
|
+
:propSet => [{ :type => self.class.wsdl_name, :all => all, :pathSet => pathSet }],
|
12
|
+
:objectSet => [{ :obj => self }],
|
13
|
+
}, :partialUpdates => false
|
14
|
+
result = @soap.propertyCollector.WaitForUpdates(version: version)
|
15
|
+
filter.DestroyPropertyFilter
|
16
|
+
changes = result.filterSet[0].objectSet[0].changeSet
|
17
|
+
changes.map { |h| [h.name.split('.').map(&:to_sym), h.val] }.each do |path,v|
|
18
|
+
k = path.pop
|
19
|
+
o = path.inject(self) { |b,k| b[k] }
|
20
|
+
o._set_property k, v unless o == self
|
21
|
+
end
|
22
|
+
result.version
|
23
|
+
end
|
24
|
+
|
25
|
+
def wait_until *pathSet, &b
|
26
|
+
ver = nil
|
27
|
+
loop do
|
28
|
+
ver = wait ver, *pathSet
|
29
|
+
if x = b.call
|
30
|
+
return x
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def collect! *props
|
36
|
+
spec = {
|
37
|
+
objectSet: [{ obj: self }],
|
38
|
+
propSet: [{
|
39
|
+
pathSet: props,
|
40
|
+
type: self.class.wsdl_name
|
41
|
+
}]
|
42
|
+
}
|
43
|
+
@soap.propertyCollector.RetrieveProperties(specSet: [spec])[0].to_hash
|
44
|
+
end
|
45
|
+
|
46
|
+
def collect *props
|
47
|
+
h = collect! *props
|
48
|
+
a = props.map { |k| h[k.to_s] }
|
49
|
+
if block_given?
|
50
|
+
yield a
|
51
|
+
else
|
52
|
+
a
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
ManagedEntity
|
58
|
+
class ManagedEntity
|
59
|
+
def path
|
60
|
+
filterSpec = VIM.PropertyFilterSpec(
|
61
|
+
objectSet: [{
|
62
|
+
obj: self,
|
63
|
+
selectSet: [
|
64
|
+
VIM.TraversalSpec(
|
65
|
+
name: 'tsME',
|
66
|
+
type: 'ManagedEntity',
|
67
|
+
path: 'parent',
|
68
|
+
skip: false,
|
69
|
+
selectSet: [
|
70
|
+
VIM.SelectionSpec(name: 'tsME')
|
71
|
+
]
|
72
|
+
)
|
73
|
+
]
|
74
|
+
}],
|
75
|
+
propSet: [{
|
76
|
+
pathSet: %w(name parent),
|
77
|
+
type: 'ManagedEntity'
|
78
|
+
}]
|
79
|
+
)
|
80
|
+
|
81
|
+
result = @soap.propertyCollector.RetrieveProperties(specSet: [filterSpec])
|
82
|
+
|
83
|
+
tree = {}
|
84
|
+
result.each { |x| tree[x.obj] = [x['parent'], x['name']] }
|
85
|
+
a = []
|
86
|
+
cur = self
|
87
|
+
while cur
|
88
|
+
parent, name = *tree[cur]
|
89
|
+
a << [cur, name]
|
90
|
+
cur = parent
|
91
|
+
end
|
92
|
+
a.reverse
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
Task
|
97
|
+
class Task
|
98
|
+
def wait_for_completion
|
99
|
+
wait_until('info.state') { %w(success error).member? info.state }
|
100
|
+
case info.state
|
101
|
+
when 'success'
|
102
|
+
info.result
|
103
|
+
when 'error'
|
104
|
+
raise info.error
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def wait_for_progress
|
109
|
+
wait_until('info.state', 'info.progress') do
|
110
|
+
yield info.progress if block_given?
|
111
|
+
%w(success error).member? info.state
|
112
|
+
end
|
113
|
+
case info.state
|
114
|
+
when 'success'
|
115
|
+
info.result
|
116
|
+
when 'error'
|
117
|
+
raise info.error
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
Folder
|
123
|
+
class Folder
|
124
|
+
def find name, type=Object
|
125
|
+
x = @soap.searchIndex.FindChild(entity: self, name: name)
|
126
|
+
x if x.is_a? type
|
127
|
+
end
|
128
|
+
|
129
|
+
def traverse! path, type=Object
|
130
|
+
traverse path, type, true
|
131
|
+
end
|
132
|
+
|
133
|
+
def traverse path, type=Object, create=false
|
134
|
+
es = path.split('/').reject(&:empty?)
|
135
|
+
return self if es.empty?
|
136
|
+
final = es.pop
|
137
|
+
|
138
|
+
p = es.inject(self) do |f,e|
|
139
|
+
f.find(e, Folder) || (create && f.CreateFolder(name: e)) || return
|
140
|
+
end
|
141
|
+
|
142
|
+
if x = p.find(final, type)
|
143
|
+
x
|
144
|
+
elsif create and type == Folder
|
145
|
+
p.CreateFolder(name: final)
|
146
|
+
else
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def children
|
152
|
+
childEntity
|
153
|
+
end
|
154
|
+
|
155
|
+
def ls
|
156
|
+
Hash[children.map { |x| [x.name, x] }]
|
157
|
+
end
|
158
|
+
|
159
|
+
def inventory propSpecs={}
|
160
|
+
propSet = [{ type: 'Folder', pathSet: ['name', 'parent'] }]
|
161
|
+
propSpecs.each do |k,v|
|
162
|
+
case k
|
163
|
+
when VIM::ManagedEntity
|
164
|
+
k = k.wsdl_name
|
165
|
+
when Symbol, String
|
166
|
+
k = k.to_s
|
167
|
+
else
|
168
|
+
fail "key must be a ManagedEntity"
|
169
|
+
end
|
170
|
+
|
171
|
+
h = { type: k }
|
172
|
+
if v == :all
|
173
|
+
h[:all] = true
|
174
|
+
elsif v.is_a? Array
|
175
|
+
h[:pathSet] = v + %w(parent)
|
176
|
+
else
|
177
|
+
fail "value must be an array of property paths or :all"
|
178
|
+
end
|
179
|
+
propSet << h
|
180
|
+
end
|
181
|
+
|
182
|
+
filterSpec = VIM.PropertyFilterSpec(
|
183
|
+
objectSet: [
|
184
|
+
obj: self,
|
185
|
+
selectSet: [
|
186
|
+
VIM.TraversalSpec(
|
187
|
+
name: 'tsFolder',
|
188
|
+
type: 'Folder',
|
189
|
+
path: 'childEntity',
|
190
|
+
skip: false,
|
191
|
+
selectSet: [
|
192
|
+
VIM.SelectionSpec(name: 'tsFolder')
|
193
|
+
]
|
194
|
+
)
|
195
|
+
]
|
196
|
+
],
|
197
|
+
propSet: propSet
|
198
|
+
)
|
199
|
+
|
200
|
+
result = @soap.propertyCollector.RetrieveProperties(specSet: [filterSpec])
|
201
|
+
|
202
|
+
tree = { self => {} }
|
203
|
+
result.each do |x|
|
204
|
+
obj = x.obj
|
205
|
+
next if obj == self
|
206
|
+
h = Hash[x.propSet.map { |y| [y.name, y.val] }]
|
207
|
+
tree[h['parent']][h['name']] = [obj, h]
|
208
|
+
tree[obj] = {} if obj.is_a? VIM::Folder
|
209
|
+
end
|
210
|
+
tree
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
Datastore
|
215
|
+
class Datastore
|
216
|
+
def datacenter
|
217
|
+
return @datacenter if @datacenter
|
218
|
+
x = parent
|
219
|
+
while not x.is_a? Datacenter
|
220
|
+
x = x.parent
|
221
|
+
end
|
222
|
+
fail unless x.is_a? Datacenter
|
223
|
+
@datacenter = x
|
224
|
+
end
|
225
|
+
|
226
|
+
def mkuripath path
|
227
|
+
"/folder/#{URI.escape path}?dcPath=#{URI.escape datacenter.name}&dsName=#{URI.escape name}"
|
228
|
+
end
|
229
|
+
|
230
|
+
def exists? path
|
231
|
+
req = Net::HTTP::Head.new mkuripath(path)
|
232
|
+
req.initialize_http_header 'cookie' => @soap.cookie
|
233
|
+
resp = @soap.http.request req
|
234
|
+
case resp
|
235
|
+
when Net::HTTPSuccess
|
236
|
+
true
|
237
|
+
when Net::HTTPNotFound
|
238
|
+
false
|
239
|
+
else
|
240
|
+
fail resp.inspect
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def download remote_path, local_path
|
245
|
+
url = "http#{@soap.http.use_ssl? ? 's' : ''}://#{@soap.http.address}:#{@soap.http.port}#{mkuripath(remote_path)}"
|
246
|
+
pid = spawn CURLBIN, "-k", '--noproxy', '*', '-f',
|
247
|
+
"-o", local_path,
|
248
|
+
"-b", @soap.cookie,
|
249
|
+
url,
|
250
|
+
out: '/dev/null'
|
251
|
+
Process.waitpid(pid, 0)
|
252
|
+
fail "download failed" unless $?.success?
|
253
|
+
end
|
254
|
+
|
255
|
+
def upload remote_path, local_path
|
256
|
+
url = "http#{@soap.http.use_ssl? ? 's' : ''}://#{@soap.http.address}:#{@soap.http.port}#{mkuripath(remote_path)}"
|
257
|
+
pid = spawn CURLBIN, "-k", '--noproxy', '*', '-f',
|
258
|
+
"-T", local_path,
|
259
|
+
"-b", @soap.cookie,
|
260
|
+
url,
|
261
|
+
out: '/dev/null'
|
262
|
+
Process.waitpid(pid, 0)
|
263
|
+
fail "upload failed" unless $?.success?
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
ServiceInstance
|
268
|
+
class ServiceInstance
|
269
|
+
def find_datacenter path=nil
|
270
|
+
if path
|
271
|
+
content.rootFolder.traverse path, VIM::Datacenter
|
272
|
+
else
|
273
|
+
content.rootFolder.childEntity.grep(VIM::Datacenter).first
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def wait_for_multiple_tasks interested, tasks
|
278
|
+
version = ''
|
279
|
+
interested = (interested + ['info.state']).uniq
|
280
|
+
task_props = Hash.new { |h,k| h[k] = {} }
|
281
|
+
|
282
|
+
filter = @soap.propertyCollector.CreateFilter :spec => {
|
283
|
+
:propSet => [{ :type => 'Task', :all => false, :pathSet => interested }],
|
284
|
+
:objectSet => tasks.map { |x| { :obj => x } },
|
285
|
+
}, :partialUpdates => false
|
286
|
+
|
287
|
+
begin
|
288
|
+
until task_props.size == tasks.size and task_props.all? { |k,h| %w(success error).member? h['info.state'] }
|
289
|
+
result = @soap.propertyCollector.WaitForUpdates(version: version)
|
290
|
+
version = result.version
|
291
|
+
os = result.filterSet[0].objectSet
|
292
|
+
|
293
|
+
os.each do |o|
|
294
|
+
changes = Hash[o.changeSet.map { |x| [x.name, x.val] }]
|
295
|
+
|
296
|
+
interested.each do |k|
|
297
|
+
task = tasks.find { |x| x._ref == o.obj._ref }
|
298
|
+
task_props[task][k] = changes[k] if changes.member? k
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
yield task_props
|
303
|
+
end
|
304
|
+
ensure
|
305
|
+
@soap.propertyCollector.CancelWaitForUpdates
|
306
|
+
filter.DestroyPropertyFilter
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
Datacenter
|
312
|
+
class Datacenter
|
313
|
+
def find_compute_resource path=nil
|
314
|
+
if path
|
315
|
+
hostFolder.traverse path, VIM::ComputeResource
|
316
|
+
else
|
317
|
+
hostFolder.childEntity.grep(VIM::ComputeResource).first
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def find_datastore name
|
322
|
+
datastore.find { |x| x.name == name }
|
323
|
+
end
|
324
|
+
|
325
|
+
def find_vm folder_path, name
|
326
|
+
vmFolder.traverse "#{folder_path}/#{name}", VIM::VirtualMachine
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
VirtualMachine
|
331
|
+
class VirtualMachine
|
332
|
+
def macs
|
333
|
+
Hash[self.config.hardware.device.grep(VIM::VirtualEthernetCard).map { |x| [x.deviceInfo.label, x.macAddress] }]
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
ObjectContent
|
338
|
+
class ObjectContent
|
339
|
+
def [](k)
|
340
|
+
to_hash[k]
|
341
|
+
end
|
342
|
+
|
343
|
+
def to_hash_uncached
|
344
|
+
h = {}
|
345
|
+
propSet.each do |x|
|
346
|
+
fail if h.member? x.name
|
347
|
+
h[x.name] = x.val
|
348
|
+
end
|
349
|
+
h
|
350
|
+
end
|
351
|
+
|
352
|
+
def to_hash
|
353
|
+
@cached_hash ||= to_hash_uncached
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
OvfManager
|
358
|
+
class OvfManager
|
359
|
+
|
360
|
+
# Parameters:
|
361
|
+
# uri
|
362
|
+
# vmName
|
363
|
+
# vmFolder
|
364
|
+
# host
|
365
|
+
# resourcePool
|
366
|
+
# datastore
|
367
|
+
# networkMappings = {}
|
368
|
+
# propertyMappings = {}
|
369
|
+
# diskProvisioning = :thin
|
370
|
+
def deployOVF opts={}
|
371
|
+
opts = { networkMappings: {},
|
372
|
+
propertyMappings: {},
|
373
|
+
diskProvisioning: :thin }.merge opts
|
374
|
+
|
375
|
+
%w(uri vmName vmFolder host resourcePool datastore).each do |k|
|
376
|
+
fail "parameter #{k} required" unless opts[k.to_sym]
|
377
|
+
end
|
378
|
+
|
379
|
+
ovfImportSpec = VIM::OvfCreateImportSpecParams(
|
380
|
+
hostSystem: opts[:host],
|
381
|
+
locale: "US",
|
382
|
+
entityName: opts[:vmName],
|
383
|
+
deploymentOption: "",
|
384
|
+
networkMapping: opts[:networkMappings].map{|from, to| VIM::OvfNetworkMapping(name: from, network: to)},
|
385
|
+
propertyMapping: opts[:propertyMappings].map{|key, value| VIM::KeyValue(key: key, value: value)},
|
386
|
+
diskProvisioning: opts[:diskProvisioning]
|
387
|
+
)
|
388
|
+
|
389
|
+
result = CreateImportSpec(
|
390
|
+
ovfDescriptor: open(opts[:uri]).read,
|
391
|
+
resourcePool: opts[:resourcePool],
|
392
|
+
datastore: opts[:datastore],
|
393
|
+
cisp: ovfImportSpec
|
394
|
+
)
|
395
|
+
|
396
|
+
raise result.error[0].localizedMessage if result.error && !result.error.empty?
|
397
|
+
|
398
|
+
if result.warning
|
399
|
+
result.warning.each{|x| puts "OVF Warning: #{x.localizedMessage.chomp}" }
|
400
|
+
end
|
401
|
+
|
402
|
+
nfcLease = opts[:resourcePool].ImportVApp(spec: result.importSpec,
|
403
|
+
folder: opts[:vmFolder],
|
404
|
+
host: opts[:host])
|
405
|
+
|
406
|
+
nfcLease.wait_until(:state) { nfcLease.state != "initializing" }
|
407
|
+
raise nfcLease.error if nfcLease.state == "error"
|
408
|
+
|
409
|
+
begin
|
410
|
+
nfcLease.HttpNfcLeaseProgress(percent: 5)
|
411
|
+
progress = 0.0
|
412
|
+
result.fileItem.each do |fileItem|
|
413
|
+
deviceUrl = nfcLease.info.deviceUrl.find{|x| x.importKey == fileItem.deviceId}
|
414
|
+
if !deviceUrl
|
415
|
+
raise "Couldn't find deviceURL for device '#{fileItem.deviceId}'"
|
416
|
+
end
|
417
|
+
|
418
|
+
# XXX handle file:// URIs
|
419
|
+
ovfFilename = opts[:uri].to_s
|
420
|
+
tmp = ovfFilename.split(/\//)
|
421
|
+
tmp.pop
|
422
|
+
tmp << fileItem.path
|
423
|
+
filename = tmp.join("/")
|
424
|
+
|
425
|
+
method = fileItem.create ? "PUT" : "POST"
|
426
|
+
|
427
|
+
href = deviceUrl.url.gsub("*", opts[:host].config.network.vnic[0].spec.ip.ipAddress)
|
428
|
+
downloadCmd = "#{CURLBIN} -L '#{filename}'"
|
429
|
+
uploadCmd = "#{CURLBIN} -X #{method} --insecure -T - -H 'Content-Type: application/x-vnd.vmware-streamVmdk' -H 'Content-Length: #{fileItem.size}' '#{href}'"
|
430
|
+
system("#{downloadCmd} | #{uploadCmd}")
|
431
|
+
progress += (95.0 / result.fileItem.length)
|
432
|
+
nfcLease.HttpNfcLeaseProgress(percent: progress.to_i)
|
433
|
+
end
|
434
|
+
|
435
|
+
nfcLease.HttpNfcLeaseProgress(percent: 100)
|
436
|
+
vm = nfcLease.info.entity
|
437
|
+
nfcLease.HttpNfcLeaseComplete
|
438
|
+
vm
|
439
|
+
end
|
440
|
+
rescue Exception
|
441
|
+
nfcLease.HttpNfcLeaseAbort
|
442
|
+
raise
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
ComputeResource
|
447
|
+
class ComputeResource
|
448
|
+
def stats
|
449
|
+
filterSpec = VIM.PropertyFilterSpec(
|
450
|
+
objectSet: [{
|
451
|
+
obj: self,
|
452
|
+
selectSet: [
|
453
|
+
VIM.TraversalSpec(
|
454
|
+
name: 'tsHosts',
|
455
|
+
type: 'ComputeResource',
|
456
|
+
path: 'host',
|
457
|
+
skip: false,
|
458
|
+
)
|
459
|
+
]
|
460
|
+
}],
|
461
|
+
propSet: [{
|
462
|
+
pathSet: %w(name overallStatus summary.hardware.cpuMhz
|
463
|
+
summary.hardware.numCpuCores summary.hardware.memorySize
|
464
|
+
summary.quickStats.overallCpuUsage
|
465
|
+
summary.quickStats.overallMemoryUsage),
|
466
|
+
type: 'HostSystem'
|
467
|
+
}]
|
468
|
+
)
|
469
|
+
|
470
|
+
result = @soap.propertyCollector.RetrieveProperties(specSet: [filterSpec])
|
471
|
+
|
472
|
+
stats = {
|
473
|
+
totalCPU: 0,
|
474
|
+
totalMem: 0,
|
475
|
+
usedCPU: 0,
|
476
|
+
usedMem: 0,
|
477
|
+
}
|
478
|
+
|
479
|
+
result.each do |x|
|
480
|
+
next if x['overallStatus'] == 'red'
|
481
|
+
stats[:totalCPU] += x['summary.hardware.cpuMhz'] * x['summary.hardware.numCpuCores']
|
482
|
+
stats[:totalMem] += x['summary.hardware.memorySize'] / (1024*1024)
|
483
|
+
stats[:usedCPU] += x['summary.quickStats.overallCpuUsage']
|
484
|
+
stats[:usedMem] += x['summary.quickStats.overallMemoryUsage']
|
485
|
+
end
|
486
|
+
|
487
|
+
stats
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
end
|