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,22 @@
|
|
1
|
+
# Copyright (c) 2010 VMware, Inc. All Rights Reserved.
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
PROFILE_TIMES = Hash.new 0.0
|
5
|
+
|
6
|
+
def profile sym, &b
|
7
|
+
start_time = Time.now
|
8
|
+
ret = b.call
|
9
|
+
elapsed = Time.now - start_time
|
10
|
+
PROFILE_TIMES[sym] += elapsed
|
11
|
+
puts "#{sym} #{elapsed}s" if $profile
|
12
|
+
ret
|
13
|
+
end
|
14
|
+
|
15
|
+
def dump_profile
|
16
|
+
PROFILE_TIMES.sort_by { |k,v| -v }.each do |sym,total|
|
17
|
+
puts "#{sym} #{total}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
at_exit { dump_profile if $profile }
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
|
3
|
+
class Trollop::Parser
|
4
|
+
def rbvmomi_connection_opts
|
5
|
+
opt :host, "host", type: :string, short: 'o', default: ENV['RBVMOMI_HOST']
|
6
|
+
opt :port, "port", type: :int, short: :none, default: (ENV.member?('RBVMOMI_PORT') ? ENV['RBVMOMI_PORT'].to_i : 443)
|
7
|
+
opt :"no-ssl", "don't use ssl", short: :none, default: (ENV['RBVMOMI_SSL'] == '0')
|
8
|
+
opt :user, "username", short: 'u', default: (ENV['RBVMOMI_USER'] || 'root')
|
9
|
+
opt :password, "password", short: 'p', default: (ENV['RBVMOMI_PASSWORD'] || '')
|
10
|
+
opt :path, "SOAP endpoint path", short: :none, default: (ENV['RBVMOMI_PATH'] || '/sdk')
|
11
|
+
opt :debug, "Log SOAP messages", short: 'd'
|
12
|
+
end
|
13
|
+
|
14
|
+
def rbvmomi_datacenter_opt
|
15
|
+
opt :datacenter, "datacenter", type: :string, short: "D", default: (ENV['RBVMOMI_DATACENTER'])
|
16
|
+
end
|
17
|
+
|
18
|
+
def rbvmomi_folder_opt
|
19
|
+
opt :folder, "VM folder", type: :string, short: "F", default: (ENV['RBVMOMI_FOLDER'] || '')
|
20
|
+
end
|
21
|
+
|
22
|
+
def rbvmomi_computer_opt
|
23
|
+
opt :computer, "Compute resource", type: :string, short: "R", default: ENV['RBVMOMI_COMPUTER']
|
24
|
+
end
|
25
|
+
|
26
|
+
def rbvmomi_datastore_opt
|
27
|
+
opt :datastore, "Datastore", short: 's', default: (ENV['RBVMOMI_DATASTORE'] || 'datstore1')
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# Copyright (c) 2010 VMware, Inc. All Rights Reserved.
|
2
|
+
require 'tokyocabinet'
|
3
|
+
require 'pp'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
include TokyoCabinet
|
7
|
+
|
8
|
+
class Class
|
9
|
+
def wsdl_name
|
10
|
+
self.class.name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module RbVmomi
|
15
|
+
|
16
|
+
module VIM
|
17
|
+
|
18
|
+
BUILTIN_TYPES = %w(ManagedObject TypeName PropertyPath ManagedObjectReference MethodName MethodFault LocalizedMethodFault)
|
19
|
+
|
20
|
+
def self.load fn
|
21
|
+
@db = HDB.new
|
22
|
+
if !@db.open(fn, HDB::OREADER | HDB::ONOLCK)
|
23
|
+
raise "VMODL db open error: #{@db.errmsg(@db.ecode)}"
|
24
|
+
end
|
25
|
+
@vmodl = Hash.new { |h,k| if e = @db[k] then h[k] = Marshal.load(e) end }
|
26
|
+
@typenames = Marshal.load(@db['_typenames']) + BUILTIN_TYPES
|
27
|
+
Object.constants.select { |x| @typenames.member? x.to_s }.each { |x| load_type x }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.has_type? name
|
31
|
+
@typenames.member? name.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.type name
|
35
|
+
if has_type? name
|
36
|
+
const_get(name.to_sym)
|
37
|
+
else
|
38
|
+
fail "no such type #{name.inspect}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.const_missing sym
|
43
|
+
if @typenames.member? sym.to_s
|
44
|
+
load_type sym
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.method_missing sym, *a
|
51
|
+
if @typenames.member? sym.to_s
|
52
|
+
const_get(sym).new *a
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.load_type sym
|
59
|
+
const_set sym, make_type(sym)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.make_type name
|
63
|
+
name = name.to_s
|
64
|
+
desc = @vmodl[name] or fail "unknown VIM type #{name}"
|
65
|
+
case desc['kind']
|
66
|
+
when 'data' then make_data_type name, desc
|
67
|
+
when 'managed' then make_managed_type name, desc
|
68
|
+
when 'enum' then make_enum_type name, desc
|
69
|
+
else fail desc.inspect
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.make_data_type name, desc
|
74
|
+
superclass = const_get(desc['wsdl_base'].to_sym)
|
75
|
+
Class.new(superclass).tap do |klass|
|
76
|
+
klass.initialize name, desc['props']
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.make_managed_type name, desc
|
81
|
+
superclass = const_get(desc['wsdl_base'].to_sym)
|
82
|
+
Class.new(superclass).tap do |klass|
|
83
|
+
klass.initialize name, desc['props'], desc['methods']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.make_enum_type name, desc
|
88
|
+
Class.new(Enum).tap do |klass|
|
89
|
+
klass.initialize name, desc['values']
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Base
|
94
|
+
class << self
|
95
|
+
attr_reader :wsdl_name
|
96
|
+
|
97
|
+
def initialize wsdl_name=self.name
|
98
|
+
@wsdl_name = wsdl_name
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_s
|
102
|
+
@wsdl_name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
initialize
|
107
|
+
end
|
108
|
+
|
109
|
+
class ObjectWithProperties < Base
|
110
|
+
class << self
|
111
|
+
attr_accessor :props_desc
|
112
|
+
|
113
|
+
def initialize name=self.name, props=[]
|
114
|
+
super name
|
115
|
+
@props_desc = props
|
116
|
+
@props_desc.each do |d|
|
117
|
+
sym = d['name'].to_sym
|
118
|
+
define_method(sym) { _get_property sym }
|
119
|
+
define_method(:"#{sym}=") { |x| _set_property sym, x }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# XXX cache
|
124
|
+
def full_props_desc
|
125
|
+
(self == ObjectWithProperties ? [] : superclass.full_props_desc) + props_desc
|
126
|
+
end
|
127
|
+
|
128
|
+
def find_prop_desc name
|
129
|
+
full_props_desc.find { |x| x['name'] == name.to_s }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def _get_property sym
|
134
|
+
fail 'unimplemented'
|
135
|
+
end
|
136
|
+
|
137
|
+
def _set_property sym, val
|
138
|
+
fail 'unimplemented'
|
139
|
+
end
|
140
|
+
|
141
|
+
initialize
|
142
|
+
end
|
143
|
+
|
144
|
+
class ObjectWithMethods < ObjectWithProperties
|
145
|
+
class << self
|
146
|
+
attr_accessor :methods_desc
|
147
|
+
|
148
|
+
def initialize name=self.name, props=[], methods={}
|
149
|
+
super name, props
|
150
|
+
@methods_desc = methods
|
151
|
+
|
152
|
+
@methods_desc.each do |k,d|
|
153
|
+
sym = k.to_sym
|
154
|
+
define_method(sym) { |*args| _call sym, *args }
|
155
|
+
define_method(:"#{sym}!") { |*args| _call sym, *args }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# XXX cache
|
160
|
+
def full_methods_desc
|
161
|
+
(self == ObjectWithMethods ? {} : superclass.full_methods_desc).merge methods_desc
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
initialize
|
166
|
+
end
|
167
|
+
|
168
|
+
class DataObject < ObjectWithProperties
|
169
|
+
attr_reader :props
|
170
|
+
|
171
|
+
def initialize props={}
|
172
|
+
@props = Hash[props.map { |k,v| [k.to_sym, v] }]
|
173
|
+
self.class.full_props_desc.each do |desc|
|
174
|
+
#fail "missing required property #{desc['name'].inspect} of #{self.class.wsdl_name}" if @props[desc['name'].to_sym].nil? and not desc['is-optional']
|
175
|
+
end
|
176
|
+
@props.each do |k,v|
|
177
|
+
fail "unexpected property name #{k}" unless self.class.find_prop_desc(k)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def _get_property sym
|
182
|
+
@props[sym]
|
183
|
+
end
|
184
|
+
|
185
|
+
def [] sym
|
186
|
+
_get_property sym
|
187
|
+
end
|
188
|
+
|
189
|
+
def _set_property sym, val
|
190
|
+
@props[sym] = val
|
191
|
+
end
|
192
|
+
|
193
|
+
def == o
|
194
|
+
return false unless o.class == self.class
|
195
|
+
keys = (props.keys + o.props.keys).uniq
|
196
|
+
keys.all? { |k| props[k] == o.props[k] }
|
197
|
+
end
|
198
|
+
|
199
|
+
def hash
|
200
|
+
props.hash
|
201
|
+
end
|
202
|
+
|
203
|
+
def pretty_print q
|
204
|
+
q.text self.class.name
|
205
|
+
q.group 2 do
|
206
|
+
q.text '('
|
207
|
+
q.breakable
|
208
|
+
props = @props.sort_by { |k,v| k.to_s }
|
209
|
+
q.seplist props, nil, :each do |k, v|
|
210
|
+
q.group do
|
211
|
+
q.text k.to_s
|
212
|
+
q.text ': '
|
213
|
+
q.pp v
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
q.breakable
|
218
|
+
q.text ')'
|
219
|
+
end
|
220
|
+
|
221
|
+
initialize
|
222
|
+
end
|
223
|
+
|
224
|
+
class ManagedObject < ObjectWithMethods
|
225
|
+
def initialize soap, ref
|
226
|
+
super()
|
227
|
+
@soap = soap
|
228
|
+
@ref = ref
|
229
|
+
end
|
230
|
+
|
231
|
+
def _ref
|
232
|
+
@ref
|
233
|
+
end
|
234
|
+
|
235
|
+
def _get_property sym
|
236
|
+
ret = @soap.propertyCollector.RetrieveProperties(:specSet => [{
|
237
|
+
:propSet => [{ :type => self.class.wsdl_name, :pathSet => [sym.to_s] }],
|
238
|
+
:objectSet => [{ :obj => self }],
|
239
|
+
}])[0]
|
240
|
+
|
241
|
+
if ret.propSet.empty?
|
242
|
+
fail if ret.missingSet.empty?
|
243
|
+
raise ret.missingSet[0].fault
|
244
|
+
else
|
245
|
+
ret.propSet[0].val
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def _set_property sym, val
|
250
|
+
fail 'unimplemented'
|
251
|
+
end
|
252
|
+
|
253
|
+
def _call method, o={}
|
254
|
+
fail "parameters must be passed as a hash" unless o.is_a? Hash
|
255
|
+
desc = self.class.full_methods_desc[method.to_s] or fail "unknown method"
|
256
|
+
@soap.call method, desc, self, o
|
257
|
+
end
|
258
|
+
|
259
|
+
def to_s
|
260
|
+
"#{self.class.wsdl_name}(#{@ref.inspect})"
|
261
|
+
end
|
262
|
+
|
263
|
+
def pretty_print pp
|
264
|
+
pp.text to_s
|
265
|
+
end
|
266
|
+
|
267
|
+
def [] k
|
268
|
+
_get_property k
|
269
|
+
end
|
270
|
+
|
271
|
+
def == x
|
272
|
+
x.class == self.class and x._ref == @ref
|
273
|
+
end
|
274
|
+
|
275
|
+
alias eql? ==
|
276
|
+
|
277
|
+
def hash
|
278
|
+
[self.class, @ref].hash
|
279
|
+
end
|
280
|
+
|
281
|
+
initialize 'ManagedObject'
|
282
|
+
end
|
283
|
+
|
284
|
+
class Enum < Base
|
285
|
+
class << self
|
286
|
+
attr_accessor :values
|
287
|
+
|
288
|
+
def initialize name=self.name, values=[]
|
289
|
+
super name
|
290
|
+
@values = values
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
attr_reader :value
|
295
|
+
|
296
|
+
def initialize value
|
297
|
+
@value = value
|
298
|
+
end
|
299
|
+
|
300
|
+
initialize
|
301
|
+
end
|
302
|
+
|
303
|
+
class MethodFault < DataObject
|
304
|
+
initialize 'MethodFault', [
|
305
|
+
{
|
306
|
+
'name' => 'faultCause',
|
307
|
+
'wsdl_type' => 'LocalizedMethodFault',
|
308
|
+
'is-array' => false,
|
309
|
+
'is-optional' => true,
|
310
|
+
}, {
|
311
|
+
'name' => 'faultMessage',
|
312
|
+
'wsdl_type' => 'LocalizableMessage',
|
313
|
+
'is-array' => true,
|
314
|
+
'is-optional' => true,
|
315
|
+
},
|
316
|
+
]
|
317
|
+
end
|
318
|
+
|
319
|
+
class LocalizedMethodFault < DataObject
|
320
|
+
initialize 'LocalizedMethodFault', [
|
321
|
+
{
|
322
|
+
'name' => 'fault',
|
323
|
+
'wsdl_type' => 'MethodFault',
|
324
|
+
'is-array' => false,
|
325
|
+
'is-optional' => false,
|
326
|
+
}, {
|
327
|
+
'name' => 'localizedMessage',
|
328
|
+
'wsdl_type' => 'xsd:string',
|
329
|
+
'is-array' => false,
|
330
|
+
'is-optional' => true,
|
331
|
+
},
|
332
|
+
]
|
333
|
+
|
334
|
+
def exception
|
335
|
+
RbVmomi.fault self.localizedMessage, self.fault
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class RuntimeFault < MethodFault
|
340
|
+
initialize
|
341
|
+
end
|
342
|
+
|
343
|
+
class MethodName < String
|
344
|
+
def self.wsdl_name; 'MethodName' end
|
345
|
+
end
|
346
|
+
|
347
|
+
class PropertyPath < String
|
348
|
+
def self.wsdl_name; 'PropertyPath' end
|
349
|
+
end
|
350
|
+
|
351
|
+
class TypeName < String
|
352
|
+
def self.wsdl_name; 'TypeName' end
|
353
|
+
end
|
354
|
+
|
355
|
+
class ManagedObjectReference
|
356
|
+
def self.wsdl_name; 'ManagedObjectReference' end
|
357
|
+
end
|
358
|
+
|
359
|
+
class ::String
|
360
|
+
def self.wsdl_name; 'xsd:string' end
|
361
|
+
end
|
362
|
+
|
363
|
+
class ::Integer
|
364
|
+
def self.wsdl_name; 'xsd:int' end
|
365
|
+
end
|
366
|
+
|
367
|
+
class ::Float
|
368
|
+
def self.wsdl_name; 'xsd:float' end
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
data/lib/trivial_soap.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Copyright (c) 2010 VMware, Inc. All Rights Reserved.
|
2
|
+
require 'rubygems'
|
3
|
+
require 'builder'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'pp'
|
7
|
+
require 'rbvmomi/profile'
|
8
|
+
|
9
|
+
class Net::HTTPGenericRequest
|
10
|
+
alias old_exec exec
|
11
|
+
|
12
|
+
def exec sock, ver, path
|
13
|
+
old_exec sock, ver, path
|
14
|
+
sock.io.flush
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class TrivialSoap
|
19
|
+
attr_accessor :debug, :cookie
|
20
|
+
attr_reader :http
|
21
|
+
|
22
|
+
def initialize opts
|
23
|
+
fail unless opts.is_a? Hash
|
24
|
+
@opts = opts
|
25
|
+
return unless @opts[:host] # for testcases
|
26
|
+
@http = Net::HTTP.new(@opts[:host], @opts[:port], @opts[:proxyHost], @opts[:proxyPort])
|
27
|
+
if @opts[:ssl]
|
28
|
+
require 'net/https'
|
29
|
+
@http.use_ssl = true
|
30
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE # XXX
|
31
|
+
@http.cert = OpenSSL::X509::Certificate.new(@opts[:cert]) if @opts[:cert]
|
32
|
+
@http.key = OpenSSL::PKey::RSA.new(@opts[:key]) if @opts[:key]
|
33
|
+
end
|
34
|
+
@http.set_debug_output(STDERR) if $DEBUG
|
35
|
+
@http.read_timeout = 60
|
36
|
+
@http.open_timeout = 5
|
37
|
+
@http.start
|
38
|
+
@debug = @opts[:debug]
|
39
|
+
@cookie = nil
|
40
|
+
@lock = Mutex.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def soap_envelope
|
44
|
+
xsd = 'http://www.w3.org/2001/XMLSchema'
|
45
|
+
env = 'http://schemas.xmlsoap.org/soap/envelope/'
|
46
|
+
xsi = 'http://www.w3.org/2001/XMLSchema-instance'
|
47
|
+
xml = Builder::XmlMarkup.new :indent => 0
|
48
|
+
xml.tag!('env:Envelope', 'xmlns:xsd' => xsd, 'xmlns:env' => env, 'xmlns:xsi' => xsi) do
|
49
|
+
xml.tag!('env:Body') do
|
50
|
+
yield xml if block_given?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
xml
|
54
|
+
end
|
55
|
+
|
56
|
+
def request action, &b
|
57
|
+
headers = { 'content-type' => 'text/xml; charset=utf-8', 'SOAPAction' => action }
|
58
|
+
headers['cookie'] = @cookie if @cookie
|
59
|
+
body = soap_envelope(&b).target!
|
60
|
+
|
61
|
+
if @debug
|
62
|
+
$stderr.puts "Request:"
|
63
|
+
$stderr.puts body
|
64
|
+
$stderr.puts
|
65
|
+
end
|
66
|
+
|
67
|
+
start_time = Time.now
|
68
|
+
response = @lock.synchronize { profile(:post) { @http.request_post(@opts[:path], body, headers) } }
|
69
|
+
end_time = Time.now
|
70
|
+
|
71
|
+
@cookie = response['set-cookie'] if response.key? 'set-cookie'
|
72
|
+
|
73
|
+
nk = Nokogiri(response.body)
|
74
|
+
|
75
|
+
if @debug
|
76
|
+
$stderr.puts "Response (in #{'%.3f' % (end_time - start_time)} s)"
|
77
|
+
$stderr.puts nk
|
78
|
+
$stderr.puts
|
79
|
+
end
|
80
|
+
|
81
|
+
nk.xpath('//soapenv:Body/*').select(&:element?).first
|
82
|
+
end
|
83
|
+
end
|