rbvmomi 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +2 -2
- data/VERSION +1 -1
- data/devel/benchmark.rb +117 -0
- data/devel/collisions.rb +18 -0
- data/devel/merge-internal-vmodl.rb +7 -0
- data/lib/rbvmomi/basic_types.rb +20 -3
- data/lib/rbvmomi/connection.rb +59 -93
- data/lib/rbvmomi/deserialization.rb +235 -0
- data/lib/rbvmomi/trivial_soap.rb +6 -3
- data/lib/rbvmomi/type_loader.rb +23 -23
- data/lib/rbvmomi/vim.rb +2 -17
- data/lib/rbvmomi/vim/ComputeResource.rb +1 -1
- data/lib/rbvmomi/vim/Datastore.rb +6 -6
- data/lib/rbvmomi/vim/Folder.rb +48 -16
- data/lib/rbvmomi/vim/HostSystem.rb +13 -1
- data/lib/rbvmomi/vim/ManagedEntity.rb +36 -25
- data/lib/rbvmomi/vim/ManagedObject.rb +3 -3
- data/lib/rbvmomi/vim/PropertyCollector.rb +4 -2
- data/lib/rbvmomi/vim/ReflectManagedMethodExecuter.rb +2 -2
- data/lib/rbvmomi/vim/ResourcePool.rb +38 -1
- data/lib/rbvmomi/vim/ServiceInstance.rb +3 -3
- data/test/test_deserialization.rb +94 -9
- data/test/test_emit_request.rb +1 -3
- data/test/test_exceptions.rb +1 -3
- data/test/test_helper.rb +14 -0
- data/test/test_misc.rb +7 -3
- data/test/test_parse_response.rb +1 -3
- data/test/test_serialization.rb +17 -5
- data/vmodl.db +0 -0
- metadata +7 -2
data/README.rdoc
CHANGED
@@ -22,7 +22,7 @@ A simple example of turning on a VM:
|
|
22
22
|
vm = dc.find_vm("myvm") or fail "VM not found"
|
23
23
|
vm.PowerOnVM_Task.wait_for_completion
|
24
24
|
|
25
|
-
This code uses several RbVmomi extensions to the
|
25
|
+
This code uses several RbVmomi extensions to the vSphere API for concision. The
|
26
26
|
expanded snippet below uses only standard API calls and should be familiar to
|
27
27
|
users of the Java SDK:
|
28
28
|
|
@@ -75,4 +75,4 @@ write something generally useful please send it to me and I'll add it in.
|
|
75
75
|
== Development
|
76
76
|
|
77
77
|
Fork the project on Github and send me a merge request, or send a patch to
|
78
|
-
rlane@vmware.com.
|
78
|
+
rlane@vmware.com. RbVmomi developers hang out in #rbvmomi on Freenode.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.5.0
|
data/devel/benchmark.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
if ENV['RBVMOMI_COVERAGE'] == '1'
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rbvmomi'
|
10
|
+
require 'rbvmomi/deserialization'
|
11
|
+
require 'benchmark'
|
12
|
+
require 'libxml'
|
13
|
+
|
14
|
+
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
|
15
|
+
|
16
|
+
VIM = RbVmomi::VIM
|
17
|
+
$conn = VIM.new(:ns => 'urn:vim25', :rev => '4.0')
|
18
|
+
raw = File.read(ARGV[0])
|
19
|
+
|
20
|
+
def diff a, b
|
21
|
+
a_io = Tempfile.new 'rbvmomi-diff-a'
|
22
|
+
b_io = Tempfile.new 'rbvmomi-diff-b'
|
23
|
+
PP.pp a, a_io
|
24
|
+
PP.pp b, b_io
|
25
|
+
a_io.close
|
26
|
+
b_io.close
|
27
|
+
system("diff -u #{a_io.path} #{b_io.path}")
|
28
|
+
a_io.unlink
|
29
|
+
b_io.unlink
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
deserializer = RbVmomi::OldDeserializer.new($conn)
|
34
|
+
end_time = Time.now + 1
|
35
|
+
n = 0
|
36
|
+
while n == 0 or end_time > Time.now
|
37
|
+
deserializer.deserialize Nokogiri::XML(raw).root
|
38
|
+
n += 1
|
39
|
+
end
|
40
|
+
N = n*10
|
41
|
+
end
|
42
|
+
|
43
|
+
puts "iterations: #{N}"
|
44
|
+
|
45
|
+
parsed_nokogiri = Nokogiri::XML(raw)
|
46
|
+
parsed_libxml = LibXML::XML::Parser.string(raw).parse
|
47
|
+
|
48
|
+
if true
|
49
|
+
nokogiri_xml = parsed_nokogiri.root
|
50
|
+
libxml_xml = parsed_libxml.root
|
51
|
+
|
52
|
+
old_nokogiri_result = RbVmomi::OldDeserializer.new($conn).deserialize nokogiri_xml
|
53
|
+
new_nokogiri_result = RbVmomi::NewDeserializer.new($conn).deserialize nokogiri_xml
|
54
|
+
new_libxml_result = RbVmomi::NewDeserializer.new($conn).deserialize libxml_xml
|
55
|
+
|
56
|
+
if new_nokogiri_result != old_nokogiri_result
|
57
|
+
puts "new_nokogiri_result doesnt match old_nokogiri_result"
|
58
|
+
puts "old_nokogiri_result:"
|
59
|
+
pp old_nokogiri_result
|
60
|
+
puts "new_nokogiri_result:"
|
61
|
+
pp new_nokogiri_result
|
62
|
+
puts "diff:"
|
63
|
+
diff old_nokogiri_result, new_nokogiri_result
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
if new_libxml_result != old_nokogiri_result
|
68
|
+
puts "new_libxml_result doesnt match old_nokogiri_result"
|
69
|
+
puts "old_nokogiri_result:"
|
70
|
+
pp old_nokogiri_result
|
71
|
+
puts "new_libxml_result:"
|
72
|
+
pp new_libxml_result
|
73
|
+
puts "diff:"
|
74
|
+
diff old_nokogiri_result, new_libxml_result
|
75
|
+
exit 1
|
76
|
+
end
|
77
|
+
|
78
|
+
puts "all results match"
|
79
|
+
end
|
80
|
+
|
81
|
+
Benchmark.bmbm do|b|
|
82
|
+
GC.start
|
83
|
+
b.report("nokogiri parsing") do
|
84
|
+
N.times { Nokogiri::XML(raw) }
|
85
|
+
end
|
86
|
+
|
87
|
+
GC.start
|
88
|
+
b.report("libxml parsing") do
|
89
|
+
N.times do
|
90
|
+
LibXML::XML::Parser.string(raw).parse
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
GC.start
|
95
|
+
b.report("old deserialization (nokogiri)") do
|
96
|
+
deserializer = RbVmomi::OldDeserializer.new($conn)
|
97
|
+
N.times do
|
98
|
+
deserializer.deserialize Nokogiri::XML(raw).root
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
GC.start
|
103
|
+
b.report("new deserialization (nokogiri)") do
|
104
|
+
deserializer = RbVmomi::NewDeserializer.new($conn)
|
105
|
+
N.times do
|
106
|
+
deserializer.deserialize Nokogiri::XML(raw).root
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
GC.start
|
111
|
+
b.report("new deserialization (libxml)") do
|
112
|
+
deserializer = RbVmomi::NewDeserializer.new($conn)
|
113
|
+
N.times do
|
114
|
+
deserializer.deserialize LibXML::XML::Parser.string(raw).parse.root
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/devel/collisions.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Find collisions between VMODL property names and Ruby methods
|
3
|
+
require 'rbvmomi'
|
4
|
+
VIM = RbVmomi::VIM
|
5
|
+
|
6
|
+
conn = VIM.new(:ns => 'urn:vim25', :rev => '4.0')
|
7
|
+
|
8
|
+
VIM.loader.typenames.each do |name|
|
9
|
+
klass = VIM.loader.get name
|
10
|
+
next unless klass.respond_to? :kind and [:managed, :data].member? klass.kind
|
11
|
+
methods = klass.kind == :managed ?
|
12
|
+
RbVmomi::BasicTypes::ObjectWithMethods.instance_methods :
|
13
|
+
RbVmomi::BasicTypes::ObjectWithProperties.instance_methods
|
14
|
+
klass.props_desc.each do |x|
|
15
|
+
name = x['name']
|
16
|
+
puts "collision: #{klass}##{name}" if methods.member? name.to_sym
|
17
|
+
end
|
18
|
+
end
|
@@ -7,6 +7,11 @@ internal_vmodl_filename = ARGV[1] or abort "internal vmodl filename required"
|
|
7
7
|
output_vmodl_filename = ARGV[2] or abort "output vmodl filename required"
|
8
8
|
|
9
9
|
TYPES = %w(
|
10
|
+
DVSKeyedOpaqueData
|
11
|
+
DVSOpaqueDataConfigSpec
|
12
|
+
DVPortgroupSelection
|
13
|
+
DVPortSelection
|
14
|
+
DVSSelection
|
10
15
|
DynamicTypeEnumTypeInfo
|
11
16
|
DynamicTypeMgrAllTypeInfo
|
12
17
|
DynamicTypeMgrAnnotation
|
@@ -27,9 +32,11 @@ ReflectManagedMethodExecuter
|
|
27
32
|
ReflectManagedMethodExecuterSoapArgument
|
28
33
|
ReflectManagedMethodExecuterSoapFault
|
29
34
|
ReflectManagedMethodExecuterSoapResult
|
35
|
+
SelectionSet
|
30
36
|
)
|
31
37
|
|
32
38
|
METHODS = %w(
|
39
|
+
DistributedVirtualSwitchManager.UpdateDvsOpaqueData_Task
|
33
40
|
HostSystem.RetrieveDynamicTypeManager
|
34
41
|
HostSystem.RetrieveManagedMethodExecuter
|
35
42
|
)
|
data/lib/rbvmomi/basic_types.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# Copyright (c) 2010 VMware, Inc. All Rights Reserved.
|
2
2
|
require 'pp'
|
3
|
+
require 'set'
|
3
4
|
|
4
5
|
module RbVmomi
|
5
6
|
module BasicTypes
|
6
7
|
|
7
|
-
BUILTIN = %w(ManagedObject DataObject TypeName PropertyPath ManagedObjectReference MethodName MethodFault LocalizedMethodFault KeyValue)
|
8
|
+
BUILTIN = Set.new %w(ManagedObject DataObject TypeName PropertyPath ManagedObjectReference MethodName MethodFault LocalizedMethodFault KeyValue)
|
8
9
|
|
9
10
|
class Base
|
10
11
|
class << self
|
@@ -36,6 +37,10 @@ class ObjectWithProperties < Base
|
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
40
|
+
def full_props_set
|
41
|
+
@full_props_set ||= Set.new(full_props_desc.map { |x| x['name'] })
|
42
|
+
end
|
43
|
+
|
39
44
|
def full_props_desc
|
40
45
|
@full_props_desc ||= (self == ObjectWithProperties ? [] : superclass.full_props_desc) + props_desc
|
41
46
|
end
|
@@ -83,11 +88,19 @@ end
|
|
83
88
|
class DataObject < ObjectWithProperties
|
84
89
|
attr_reader :props
|
85
90
|
|
91
|
+
def self.kind; :data end
|
92
|
+
|
86
93
|
def initialize props={}
|
94
|
+
# Deserialization fast path
|
95
|
+
if props == nil
|
96
|
+
@props = {}
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
87
100
|
@props = Hash[props.map { |k,v| [k.to_sym, v] }]
|
88
|
-
self.class.full_props_desc.each do |desc|
|
101
|
+
#self.class.full_props_desc.each do |desc|
|
89
102
|
#fail "missing required property #{desc['name'].inspect} of #{self.class.wsdl_name}" if @props[desc['name'].to_sym].nil? and not desc['is-optional']
|
90
|
-
end
|
103
|
+
#end
|
91
104
|
@props.each do |k,v|
|
92
105
|
fail "unexpected property name #{k}" unless self.class.find_prop_desc(k)
|
93
106
|
end
|
@@ -148,6 +161,8 @@ class DataObject < ObjectWithProperties
|
|
148
161
|
end
|
149
162
|
|
150
163
|
class ManagedObject < ObjectWithMethods
|
164
|
+
def self.kind; :managed end
|
165
|
+
|
151
166
|
def initialize connection, ref
|
152
167
|
super()
|
153
168
|
@connection = connection
|
@@ -222,6 +237,8 @@ class Enum < Base
|
|
222
237
|
end
|
223
238
|
end
|
224
239
|
|
240
|
+
def self.kind; :enum end
|
241
|
+
|
225
242
|
attr_reader :value
|
226
243
|
|
227
244
|
def initialize value
|
data/lib/rbvmomi/connection.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# Copyright (c) 2010 VMware, Inc. All Rights Reserved.
|
2
2
|
require 'time'
|
3
|
+
require 'date'
|
3
4
|
require 'rbvmomi/trivial_soap'
|
4
5
|
require 'rbvmomi/basic_types'
|
5
6
|
require 'rbvmomi/fault'
|
6
7
|
require 'rbvmomi/type_loader'
|
8
|
+
require 'rbvmomi/deserialization'
|
7
9
|
|
8
10
|
module RbVmomi
|
9
11
|
|
@@ -15,12 +17,24 @@ class Connection < TrivialSoap
|
|
15
17
|
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
|
16
18
|
|
17
19
|
attr_accessor :rev
|
18
|
-
|
20
|
+
attr_reader :profile
|
21
|
+
attr_reader :profile_summary
|
22
|
+
attr_accessor :profiling
|
23
|
+
attr_reader :deserializer
|
24
|
+
|
19
25
|
def initialize opts
|
20
26
|
@ns = opts[:ns] or fail "no namespace specified"
|
21
27
|
@rev = opts[:rev] or fail "no revision specified"
|
28
|
+
@deserializer = Deserializer.new self
|
29
|
+
reset_profiling
|
30
|
+
@profiling = false
|
22
31
|
super opts
|
23
32
|
end
|
33
|
+
|
34
|
+
def reset_profiling
|
35
|
+
@profile = {}
|
36
|
+
@profile_summary = {:network_latency => 0, :request_emit => 0, :response_parse => 0, :num_calls => 0}
|
37
|
+
end
|
24
38
|
|
25
39
|
def emit_request xml, method, descs, this, params
|
26
40
|
xml.tag! method, :xmlns => @ns do
|
@@ -41,7 +55,7 @@ class Connection < TrivialSoap
|
|
41
55
|
def parse_response resp, desc
|
42
56
|
if resp.at('faultcode')
|
43
57
|
detail = resp.at('detail')
|
44
|
-
fault = detail &&
|
58
|
+
fault = detail && @deserializer.deserialize(detail.children.first, 'MethodFault')
|
45
59
|
msg = resp.at('faultstring').text
|
46
60
|
if fault
|
47
61
|
raise RbVmomi::Fault.new(msg, fault)
|
@@ -51,7 +65,7 @@ class Connection < TrivialSoap
|
|
51
65
|
else
|
52
66
|
if desc
|
53
67
|
type = desc['is-task'] ? 'Task' : desc['wsdl_type']
|
54
|
-
returnvals = resp.children.select(&:element?).map { |c|
|
68
|
+
returnvals = resp.children.select(&:element?).map { |c| @deserializer.deserialize c, type }
|
55
69
|
(desc['is-array'] && !desc['is-task']) ? returnvals : returnvals.first
|
56
70
|
else
|
57
71
|
nil
|
@@ -64,89 +78,38 @@ class Connection < TrivialSoap
|
|
64
78
|
fail "parameters must be passed as a hash" unless params.is_a? Hash
|
65
79
|
fail unless desc.is_a? Hash
|
66
80
|
|
67
|
-
|
81
|
+
t1 = Time.now
|
82
|
+
body = soap_envelope do |xml|
|
68
83
|
emit_request xml, method, desc['params'], this, params
|
84
|
+
end.target!
|
85
|
+
|
86
|
+
t2 = Time.now
|
87
|
+
resp, resp_size = request "#{@ns}/#{@rev}", body
|
88
|
+
|
89
|
+
t3 = Time.now
|
90
|
+
out = parse_response resp, desc['result']
|
91
|
+
|
92
|
+
if @profiling
|
93
|
+
t4 = Time.now
|
94
|
+
@profile[method] ||= []
|
95
|
+
profile_info = {
|
96
|
+
:network_latency => (t3 - t2),
|
97
|
+
:request_emit => t2 - t1,
|
98
|
+
:response_parse => t4 - t3,
|
99
|
+
:params => params,
|
100
|
+
:obj => this,
|
101
|
+
:backtrace => caller,
|
102
|
+
:request_size => body.length,
|
103
|
+
:response_size => resp_size,
|
104
|
+
}
|
105
|
+
@profile[method] << profile_info
|
106
|
+
@profile_summary[:network_latency] += profile_info[:network_latency]
|
107
|
+
@profile_summary[:response_parse] += profile_info[:response_parse]
|
108
|
+
@profile_summary[:request_emit] += profile_info[:request_emit]
|
109
|
+
@profile_summary[:num_calls] += 1
|
69
110
|
end
|
70
|
-
|
71
|
-
|
72
|
-
end
|
73
|
-
|
74
|
-
def demangle_array_type x
|
75
|
-
case x
|
76
|
-
when 'AnyType' then 'anyType'
|
77
|
-
when 'DateTime' then 'dateTime'
|
78
|
-
when 'Boolean', 'String', 'Byte', 'Short', 'Int', 'Long', 'Float', 'Double' then x.downcase
|
79
|
-
else x
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def xml2obj xml, typename
|
84
|
-
if IS_JRUBY
|
85
|
-
type_attr = xml.attribute_nodes.find { |a| a.name == 'type' &&
|
86
|
-
a.namespace &&
|
87
|
-
a.namespace.prefix == 'xsi' }
|
88
|
-
else
|
89
|
-
type_attr = xml.attribute_with_ns('type', NS_XSI)
|
90
|
-
end
|
91
|
-
typename = (type_attr || typename).to_s
|
92
|
-
|
93
|
-
if typename =~ /^ArrayOf/
|
94
|
-
typename = demangle_array_type $'
|
95
|
-
return xml.children.select(&:element?).map { |c| xml2obj c, typename }
|
96
|
-
end
|
97
|
-
|
98
|
-
t = type typename
|
99
|
-
if t <= BasicTypes::DataObject
|
100
|
-
props_desc = t.full_props_desc
|
101
|
-
h = {}
|
102
|
-
props_desc.select { |d| d['is-array'] }.each { |d| h[d['name'].to_sym] = [] }
|
103
|
-
xml.children.each do |c|
|
104
|
-
next unless c.element?
|
105
|
-
field = c.name.to_sym
|
106
|
-
d = t.find_prop_desc(field.to_s) or next
|
107
|
-
o = xml2obj c, d['wsdl_type']
|
108
|
-
if h[field].is_a? Array
|
109
|
-
h[field] << o
|
110
|
-
else
|
111
|
-
h[field] = o
|
112
|
-
end
|
113
|
-
end
|
114
|
-
t.new h
|
115
|
-
elsif t == BasicTypes::ManagedObjectReference
|
116
|
-
type(xml['type']).new self, xml.text
|
117
|
-
elsif t <= BasicTypes::ManagedObject
|
118
|
-
type(xml['type'] || t.wsdl_name).new self, xml.text
|
119
|
-
elsif t <= BasicTypes::Enum
|
120
|
-
xml.text
|
121
|
-
elsif t <= BasicTypes::KeyValue
|
122
|
-
h = {}
|
123
|
-
xml.children.each do |c|
|
124
|
-
next unless c.element?
|
125
|
-
h[c.name] = c.text
|
126
|
-
end
|
127
|
-
[h['key'], h['value']]
|
128
|
-
elsif t <= String
|
129
|
-
xml.text
|
130
|
-
elsif t <= Symbol
|
131
|
-
xml.text.to_sym
|
132
|
-
elsif t <= Integer
|
133
|
-
xml.text.to_i
|
134
|
-
elsif t <= Float
|
135
|
-
xml.text.to_f
|
136
|
-
elsif t <= Time
|
137
|
-
Time.parse xml.text
|
138
|
-
elsif t == BasicTypes::Boolean
|
139
|
-
xml.text == 'true' || xml.text == '1'
|
140
|
-
elsif t == BasicTypes::Binary
|
141
|
-
xml.text.unpack('m')[0]
|
142
|
-
elsif t == BasicTypes::AnyType
|
143
|
-
fail "attempted to deserialize an AnyType"
|
144
|
-
else fail "unexpected type #{t.inspect}"
|
145
|
-
end
|
146
|
-
rescue
|
147
|
-
$stderr.puts "#{$!.class} while deserializing #{xml.name} (#{typename}):"
|
148
|
-
$stderr.puts xml.to_s
|
149
|
-
raise
|
111
|
+
|
112
|
+
out
|
150
113
|
end
|
151
114
|
|
152
115
|
# hic sunt dracones
|
@@ -207,7 +170,10 @@ class Connection < TrivialSoap
|
|
207
170
|
xml.tag! name, o.to_s, attrs
|
208
171
|
when DateTime
|
209
172
|
attrs['xsi:type'] = 'xsd:dateTime' if expected == BasicTypes::AnyType
|
210
|
-
xml.tag! name, o.
|
173
|
+
xml.tag! name, o.strftime('%FT%T%:z'), attrs
|
174
|
+
when Time
|
175
|
+
attrs['xsi:type'] = 'xsd:dateTime' if expected == BasicTypes::AnyType
|
176
|
+
xml.tag! name, o.iso8601, attrs
|
211
177
|
else fail "unexpected object class #{o.class}"
|
212
178
|
end
|
213
179
|
xml
|
@@ -230,7 +196,7 @@ class Connection < TrivialSoap
|
|
230
196
|
when :base64Binary then BasicTypes::Binary
|
231
197
|
when :KeyValue then BasicTypes::KeyValue
|
232
198
|
else
|
233
|
-
if @loader.
|
199
|
+
if @loader.has? name
|
234
200
|
const_get(name)
|
235
201
|
else
|
236
202
|
fail "no such type #{name.inspect}"
|
@@ -252,25 +218,25 @@ protected
|
|
252
218
|
|
253
219
|
def self.const_missing sym
|
254
220
|
name = sym.to_s
|
255
|
-
if @loader and @loader.
|
256
|
-
@loader.
|
257
|
-
const_get sym
|
221
|
+
if @loader and @loader.has? name
|
222
|
+
@loader.get(name)
|
258
223
|
else
|
259
224
|
super
|
260
225
|
end
|
261
226
|
end
|
262
227
|
|
263
228
|
def self.method_missing sym, *a
|
264
|
-
|
265
|
-
|
229
|
+
name = sym.to_s
|
230
|
+
if @loader and @loader.has? name
|
231
|
+
@loader.get(name).new(*a)
|
266
232
|
else
|
267
233
|
super
|
268
234
|
end
|
269
235
|
end
|
270
236
|
|
271
237
|
def self.load_vmodl fn
|
272
|
-
@loader = RbVmomi::TypeLoader.new
|
273
|
-
|
238
|
+
@loader = RbVmomi::TypeLoader.new fn, extension_dirs, self
|
239
|
+
nil
|
274
240
|
end
|
275
241
|
end
|
276
242
|
|