fog-opennebula 0.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.
- checksums.yaml +7 -0
- data/.gitignore +32 -0
- data/CONTRIBUTORS.md +4 -0
- data/Gemfile +9 -0
- data/LICENSE.md +20 -0
- data/README.md +95 -0
- data/Rakefile +118 -0
- data/fog-opennebula.gemspec +35 -0
- data/lib/fog/bin/opennebula.rb +32 -0
- data/lib/fog/opennebula.rb +30 -0
- data/lib/fog/opennebula/compute.rb +136 -0
- data/lib/fog/opennebula/models/compute/flavor.rb +190 -0
- data/lib/fog/opennebula/models/compute/flavors.rb +46 -0
- data/lib/fog/opennebula/models/compute/group.rb +28 -0
- data/lib/fog/opennebula/models/compute/groups.rb +38 -0
- data/lib/fog/opennebula/models/compute/interface.rb +39 -0
- data/lib/fog/opennebula/models/compute/interfaces.rb +20 -0
- data/lib/fog/opennebula/models/compute/network.rb +48 -0
- data/lib/fog/opennebula/models/compute/networks.rb +42 -0
- data/lib/fog/opennebula/models/compute/server.rb +85 -0
- data/lib/fog/opennebula/models/compute/servers.rb +33 -0
- data/lib/fog/opennebula/requests/compute/OpenNebulaVNC.rb +314 -0
- data/lib/fog/opennebula/requests/compute/get_vnc_console.rb +58 -0
- data/lib/fog/opennebula/requests/compute/image_pool.rb +33 -0
- data/lib/fog/opennebula/requests/compute/list_groups.rb +87 -0
- data/lib/fog/opennebula/requests/compute/list_networks.rb +79 -0
- data/lib/fog/opennebula/requests/compute/list_vms.rb +79 -0
- data/lib/fog/opennebula/requests/compute/template_pool.rb +120 -0
- data/lib/fog/opennebula/requests/compute/vm_allocate.rb +97 -0
- data/lib/fog/opennebula/requests/compute/vm_destroy.rb +39 -0
- data/lib/fog/opennebula/requests/compute/vm_disk_snapshot.rb +33 -0
- data/lib/fog/opennebula/requests/compute/vm_resume.rb +35 -0
- data/lib/fog/opennebula/requests/compute/vm_shutdown.rb +22 -0
- data/lib/fog/opennebula/requests/compute/vm_stop.rb +21 -0
- data/lib/fog/opennebula/requests/compute/vm_suspend.rb +38 -0
- data/lib/fog/opennebula/version.rb +9 -0
- data/tests/opennebula/compute_tests.rb +15 -0
- data/tests/opennebula/models/compute/flavor_tests.rb +34 -0
- data/tests/opennebula/models/compute/flavors_tests.rb +15 -0
- data/tests/opennebula/models/compute/group_tests.rb +25 -0
- data/tests/opennebula/models/compute/groups_tests.rb +14 -0
- data/tests/opennebula/models/compute/network_tests.rb +24 -0
- data/tests/opennebula/models/compute/networks_tests.rb +14 -0
- data/tests/opennebula/requests/compute/vm_allocate_tests.rb +70 -0
- data/tests/opennebula/requests/compute/vm_disk_snapshot_test.rb +44 -0
- data/tests/opennebula/requests/compute/vm_suspend_resume_tests.rb +45 -0
- metadata +243 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module Fog
|
2
|
+
|
3
|
+
module Compute
|
4
|
+
|
5
|
+
class OpenNebula
|
6
|
+
|
7
|
+
class Real
|
8
|
+
|
9
|
+
def image_pool(filter = {})
|
10
|
+
images = ::OpenNebula::ImagePool.new(client)
|
11
|
+
if filter[:mine].nil?
|
12
|
+
images.info!
|
13
|
+
else
|
14
|
+
images.info_mine!
|
15
|
+
end
|
16
|
+
|
17
|
+
unless filter[:id].nil?
|
18
|
+
images.each do |i|
|
19
|
+
if filter[:id] == i.id
|
20
|
+
return [i] # return an array with only one element - found image
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
images
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Fog
|
2
|
+
|
3
|
+
module Compute
|
4
|
+
|
5
|
+
class OpenNebula
|
6
|
+
|
7
|
+
class Real
|
8
|
+
|
9
|
+
def list_groups(filter = {})
|
10
|
+
groups = []
|
11
|
+
grouppool = ::OpenNebula::GroupPool.new(client)
|
12
|
+
grouppool.info
|
13
|
+
|
14
|
+
# {
|
15
|
+
# "GROUP"=>{
|
16
|
+
# "ID"=>"0",
|
17
|
+
# "NAME"=>"oneadmin",
|
18
|
+
# "USERS"=>{"ID"=>["0", "1"]},
|
19
|
+
# "DATASTORE_QUOTA"=>{},
|
20
|
+
# "NETWORK_QUOTA"=>{},
|
21
|
+
# "VM_QUOTA"=>{},
|
22
|
+
# "IMAGE_QUOTA"=>{}
|
23
|
+
# }
|
24
|
+
# }
|
25
|
+
|
26
|
+
grouppool.each do |group|
|
27
|
+
filter_missmatch = false
|
28
|
+
|
29
|
+
unless filter.empty?
|
30
|
+
filter.each do |k, v|
|
31
|
+
if group[k.to_s.upcase.to_s] && group[k.to_s.upcase.to_s] != v.to_s
|
32
|
+
filter_missmatch = true
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
next if filter_missmatch
|
37
|
+
end
|
38
|
+
groups << { :id => group['ID'], :name => group['NAME'] }
|
39
|
+
end
|
40
|
+
groups
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class Mock
|
46
|
+
|
47
|
+
def list_groups(filter = {})
|
48
|
+
groups = []
|
49
|
+
net1 = mock_group '1', 'net1'
|
50
|
+
net2 = mock_group '2', 'fogtest'
|
51
|
+
|
52
|
+
grouppool = [net1, net2]
|
53
|
+
grouppool.each do |group|
|
54
|
+
filter_missmatch = false
|
55
|
+
|
56
|
+
unless filter.empty?
|
57
|
+
filter.each do |k, v|
|
58
|
+
if group[k.to_s.upcase.to_s] && group[k.to_s.upcase.to_s] != v.to_s
|
59
|
+
filter_missmatch = true
|
60
|
+
break
|
61
|
+
end
|
62
|
+
end
|
63
|
+
next if filter_missmatch
|
64
|
+
end
|
65
|
+
groups << { :id => group['ID'], :name => group['NAME'] }
|
66
|
+
end
|
67
|
+
groups
|
68
|
+
end
|
69
|
+
|
70
|
+
def mock_group(id, name)
|
71
|
+
{
|
72
|
+
'ID' => id,
|
73
|
+
'NAME' => name,
|
74
|
+
'UID' => '5',
|
75
|
+
'GID' => '5',
|
76
|
+
'DESCRIPTION' => 'netDescription',
|
77
|
+
'VLAN' => '5'
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Fog
|
2
|
+
|
3
|
+
module Compute
|
4
|
+
|
5
|
+
class OpenNebula
|
6
|
+
|
7
|
+
class Real
|
8
|
+
|
9
|
+
def list_networks(filter = {})
|
10
|
+
networks = []
|
11
|
+
netpool = ::OpenNebula::VirtualNetworkPool.new(client)
|
12
|
+
if filter[:id].nil?
|
13
|
+
netpool.info!(-2, -1, -1)
|
14
|
+
elsif filter[:id]
|
15
|
+
filter[:id] = filter[:id].to_i if filter[:id].is_a?(String)
|
16
|
+
netpool.info!(-2, filter[:id], filter[:id])
|
17
|
+
end
|
18
|
+
|
19
|
+
netpool.each do |network|
|
20
|
+
if filter[:network] && filter[:network].is_a?(String) && !filter[:network].empty?
|
21
|
+
next if network.to_hash['VNET']['NAME'] != filter[:network]
|
22
|
+
end
|
23
|
+
if filter[:network_uname] && filter[:network_uname].is_a?(String) && !filter[:network_uname].empty?
|
24
|
+
next if network.to_hash['VNET']['UNAME'] != filter[:network_uname]
|
25
|
+
end
|
26
|
+
if filter[:network_uid] && filter[:network_uid].is_a?(String) && !filter[:network_uid].empty?
|
27
|
+
next if network.to_hash['VNET']['UID'] != filter[:network_uid]
|
28
|
+
end
|
29
|
+
networks << network_to_attributes(network.to_hash)
|
30
|
+
end
|
31
|
+
networks
|
32
|
+
end
|
33
|
+
|
34
|
+
def network_to_attributes(net)
|
35
|
+
return if net.nil?
|
36
|
+
|
37
|
+
h = {
|
38
|
+
:id => net['VNET']['ID'],
|
39
|
+
:name => net['VNET']['NAME'],
|
40
|
+
:uid => net['VNET']['UID'],
|
41
|
+
:uname => net['VNET']['UNAME'],
|
42
|
+
:gid => net['VNET']['GID']
|
43
|
+
}
|
44
|
+
|
45
|
+
h[:description] = net['VNET']['TEMPLATE']['DESCRIPTION'] unless net['VNET']['TEMPLATE']['DESCRIPTION'].nil?
|
46
|
+
h[:vlan] = net['VNET']['VLAN_ID'] unless net['VNET']['VLAN_ID'].nil? || net['VNET']['VLAN_ID'].empty?
|
47
|
+
|
48
|
+
h
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
class Mock
|
54
|
+
|
55
|
+
def list_networks(_filters = {})
|
56
|
+
net1 = mock_network 'fogtest'
|
57
|
+
net2 = mock_network 'net2'
|
58
|
+
[net1, net2]
|
59
|
+
end
|
60
|
+
|
61
|
+
def mock_network(name)
|
62
|
+
{
|
63
|
+
:id => '5',
|
64
|
+
:name => name,
|
65
|
+
:uid => '5',
|
66
|
+
:uname => 'mock',
|
67
|
+
:gid => '5',
|
68
|
+
:description => 'netDescription',
|
69
|
+
:vlan => '5'
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Fog
|
2
|
+
|
3
|
+
module Compute
|
4
|
+
|
5
|
+
class OpenNebula
|
6
|
+
|
7
|
+
class Real
|
8
|
+
|
9
|
+
def list_vms(filter = {})
|
10
|
+
vms = []
|
11
|
+
vmpool = ::OpenNebula::VirtualMachinePool.new(client)
|
12
|
+
if filter[:id].nil?
|
13
|
+
vmpool.info(-2, -1, -1, -1)
|
14
|
+
elsif filter[:id]
|
15
|
+
filter[:id] = filter[:id].to_i if filter[:id].is_a?(String)
|
16
|
+
vmpool.info(-2, filter[:id], filter[:id], -1)
|
17
|
+
end
|
18
|
+
|
19
|
+
vmpool.each do |vm|
|
20
|
+
one = vm.to_hash
|
21
|
+
data = {}
|
22
|
+
data['onevm_object'] = vm
|
23
|
+
data['status'] = vm.state
|
24
|
+
data['state'] = vm.lcm_state_str
|
25
|
+
data['id'] = vm.id
|
26
|
+
data['gid'] = vm.gid
|
27
|
+
data['uuid'] = vm.id
|
28
|
+
data['name'] = one['VM']['NAME'] unless one['VM']['NAME'].nil?
|
29
|
+
data['user'] = one['VM']['UNAME'] unless one['VM']['UNAME'].nil?
|
30
|
+
data['group'] = one['VM']['GNAME'] unless one['VM']['GNAME'].nil?
|
31
|
+
|
32
|
+
unless one['VM']['TEMPLATE'].nil?
|
33
|
+
data['cpu'] = one['VM']['TEMPLATE']['VCPU'] unless one['VM']['TEMPLATE']['VCPU'].nil?
|
34
|
+
data['memory'] = one['VM']['TEMPLATE']['MEMORY'] unless one['VM']['TEMPLATE']['MEMORY'].nil?
|
35
|
+
unless one['VM']['TEMPLATE']['NIC'].nil?
|
36
|
+
if one['VM']['TEMPLATE']['NIC'].is_a?(Array)
|
37
|
+
data['ip'] = one['VM']['TEMPLATE']['NIC'][0]['IP']
|
38
|
+
data['mac'] = one['VM']['TEMPLATE']['NIC'][0]['MAC']
|
39
|
+
else
|
40
|
+
data['ip'] = one['VM']['TEMPLATE']['NIC']['IP'] unless one['VM']['TEMPLATE']['NIC']['IP'].nil?
|
41
|
+
data['mac'] = one['VM']['TEMPLATE']['NIC']['MAC'] unless one['VM']['TEMPLATE']['NIC']['MAC'].nil?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
vms << data
|
47
|
+
end
|
48
|
+
vms
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
module Shared
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
class Mock
|
60
|
+
|
61
|
+
def list_vms(filter = {})
|
62
|
+
vms = []
|
63
|
+
data['vms'].each do |vm|
|
64
|
+
if filter[:id].nil?
|
65
|
+
vms << vm
|
66
|
+
elsif filter[:id] == vm['id']
|
67
|
+
vms << vm
|
68
|
+
end
|
69
|
+
end
|
70
|
+
vms
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Fog
|
2
|
+
|
3
|
+
module Compute
|
4
|
+
|
5
|
+
class OpenNebula
|
6
|
+
|
7
|
+
class Real
|
8
|
+
|
9
|
+
def template_pool(filter = {})
|
10
|
+
templates = ::OpenNebula::TemplatePool.new(client)
|
11
|
+
if filter[:id].nil?
|
12
|
+
templates.info!(-2, -1, -1)
|
13
|
+
elsif filter[:id]
|
14
|
+
filter[:id] = filter[:id].to_i if filter[:id].is_a?(String)
|
15
|
+
templates.info!(-2, filter[:id], filter[:id])
|
16
|
+
end
|
17
|
+
|
18
|
+
templates = templates.map do |t|
|
19
|
+
# filtering by name
|
20
|
+
# done here, because OpenNebula:TemplatePool does not support something like .delete_if
|
21
|
+
if filter[:name] && filter[:name].is_a?(String) && !filter[:name].empty?
|
22
|
+
next if t.to_hash['VMTEMPLATE']['NAME'] != filter[:name]
|
23
|
+
end
|
24
|
+
if filter[:uname] && filter[:uname].is_a?(String) && !filter[:uname].empty?
|
25
|
+
next if t.to_hash['VMTEMPLATE']['UNAME'] != filter[:uname]
|
26
|
+
end
|
27
|
+
if filter[:uid] && filter[:uid].is_a?(String) && !filter[:uid].empty?
|
28
|
+
next if t.to_hash['VMTEMPLATE']['UID'] != filter[:uid]
|
29
|
+
end
|
30
|
+
|
31
|
+
h = Hash[
|
32
|
+
:id => t.to_hash['VMTEMPLATE']['ID'],
|
33
|
+
:name => t.to_hash['VMTEMPLATE']['NAME'],
|
34
|
+
:content => t.template_str,
|
35
|
+
:USER_VARIABLES => '' # Default if not set in template
|
36
|
+
]
|
37
|
+
h.merge! t.to_hash['VMTEMPLATE']['TEMPLATE']
|
38
|
+
|
39
|
+
# h["NIC"] has to be an array of nic objects
|
40
|
+
nics = h['NIC'] unless h['NIC'].nil?
|
41
|
+
h['NIC'] = [] # reset nics to a array
|
42
|
+
if nics.is_a? Array
|
43
|
+
nics.each do |n|
|
44
|
+
if n['NETWORK_ID']
|
45
|
+
vnet = networks.get(n['NETWORK_ID'].to_s)
|
46
|
+
elsif n['NETWORK']
|
47
|
+
vnet = networks.get_by_name(n['NETWORK'].to_s)
|
48
|
+
else
|
49
|
+
next
|
50
|
+
end
|
51
|
+
h['NIC'] << interfaces.new(:vnet => vnet, :model => n['MODEL'] || 'virtio')
|
52
|
+
end
|
53
|
+
elsif nics.is_a? Hash
|
54
|
+
nics['model'] = 'virtio' if nics['model'].nil?
|
55
|
+
# nics["uuid"] = "0" if nics["uuid"].nil? # is it better is to remove this NIC?
|
56
|
+
n = networks.get_by_filter(
|
57
|
+
:id => nics['NETWORK_ID'],
|
58
|
+
:network => nics['NETWORK'],
|
59
|
+
:network_uname => nics['NETWORK_UNAME'],
|
60
|
+
:network_uid => nics['NETWORK_UID']
|
61
|
+
)
|
62
|
+
n.each do |i|
|
63
|
+
h['NIC'] << interfaces.new(:vnet => i)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# every key should be lowercase
|
68
|
+
ret_hash = {}
|
69
|
+
h.each_pair do |k, v|
|
70
|
+
ret_hash.merge!(k.downcase => v)
|
71
|
+
end
|
72
|
+
ret_hash
|
73
|
+
end
|
74
|
+
|
75
|
+
templates.delete nil
|
76
|
+
raise Fog::Compute::OpenNebula::NotFound, 'Flavor/Template not found' if templates.empty?
|
77
|
+
|
78
|
+
templates
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class Mock
|
84
|
+
|
85
|
+
def template_pool(_filter = {})
|
86
|
+
nic1 = Mock_nic.new
|
87
|
+
nic1.vnet = networks.first
|
88
|
+
|
89
|
+
data['template_pool']
|
90
|
+
data['template_pool'].each do |tmpl|
|
91
|
+
tmpl['nic'][0] = nic1
|
92
|
+
end
|
93
|
+
data['template_pool']
|
94
|
+
end
|
95
|
+
|
96
|
+
class Mock_nic
|
97
|
+
|
98
|
+
attr_accessor :vnet
|
99
|
+
|
100
|
+
def id
|
101
|
+
2
|
102
|
+
end
|
103
|
+
|
104
|
+
def name
|
105
|
+
'fogtest'
|
106
|
+
end
|
107
|
+
|
108
|
+
def model
|
109
|
+
'virtio-net'
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Fog
|
2
|
+
|
3
|
+
module Compute
|
4
|
+
|
5
|
+
class OpenNebula
|
6
|
+
|
7
|
+
class Real
|
8
|
+
|
9
|
+
def vm_allocate(attr = {})
|
10
|
+
if attr[:flavor].nil?
|
11
|
+
raise ArgumentError, "Attribute flavor is nil! #{attr.inspect}"
|
12
|
+
end
|
13
|
+
if attr[:name].nil? || attr[:name].empty?
|
14
|
+
raise ArgumentError, "Attribute name is nil or empty! #{attr.inspect}"
|
15
|
+
end
|
16
|
+
|
17
|
+
xml = ::OpenNebula::VirtualMachine.build_xml
|
18
|
+
vm = ::OpenNebula::VirtualMachine.new(xml, client)
|
19
|
+
rc = vm.allocate(attr[:flavor].to_s + "\nNAME=\"" + attr[:name] + '"')
|
20
|
+
|
21
|
+
raise(rc) if rc.is_a? ::OpenNebula::Error
|
22
|
+
|
23
|
+
# -1 - do not change the owner
|
24
|
+
vm.chown(-1, attr[:gid].to_i) unless attr[:gid].nil?
|
25
|
+
|
26
|
+
# TODO
|
27
|
+
# check if vm is created vmid.class == One error class
|
28
|
+
vm.info!
|
29
|
+
|
30
|
+
one = vm.to_hash
|
31
|
+
data = {}
|
32
|
+
data['onevm_object'] = vm
|
33
|
+
data['status'] = vm.state
|
34
|
+
data['state'] = vm.lcm_state_str
|
35
|
+
data['id'] = vm.id
|
36
|
+
data['uuid'] = vm.id
|
37
|
+
data['gid'] = vm.gid
|
38
|
+
data['name'] = one['VM']['NAME'] unless one['VM']['NAME'].nil?
|
39
|
+
data['user'] = one['VM']['UNAME'] unless one['VM']['UNAME'].nil?
|
40
|
+
data['group'] = one['VM']['GNAME'] unless one['VM']['GNAME'].nil?
|
41
|
+
|
42
|
+
unless one['VM']['TEMPLATE'].nil?
|
43
|
+
temp = one['VM']['TEMPLATE']
|
44
|
+
data['cpu'] = temp['VCPU'] unless temp['VCPU'].nil?
|
45
|
+
data['memory'] = temp['MEMORY'] unless temp['MEMORY'].nil?
|
46
|
+
unless temp['NIC'].nil?
|
47
|
+
if one['VM']['TEMPLATE']['NIC'].is_a?(Array)
|
48
|
+
data['mac'] = temp['NIC'][0]['MAC'] unless temp['NIC'][0]['MAC'].nil?
|
49
|
+
data['ip'] = temp['NIC'][0]['IP'] unless temp['NIC'][0]['IP'].nil?
|
50
|
+
else
|
51
|
+
data['mac'] = temp['NIC']['MAC'] unless temp['NIC']['MAC'].nil?
|
52
|
+
data['ip'] = temp['NIC']['IP'] unless temp['NIC']['IP'].nil?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
data
|
58
|
+
rescue StandardError => err
|
59
|
+
raise(err)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class Mock
|
65
|
+
|
66
|
+
def vm_allocate(attr = {})
|
67
|
+
response = Excon::Response.new
|
68
|
+
response.status = 200
|
69
|
+
|
70
|
+
id = rand(1000)
|
71
|
+
ids = []
|
72
|
+
|
73
|
+
data['vms'].each do |vm|
|
74
|
+
ids << vm['id']
|
75
|
+
next unless vm['id'] == id
|
76
|
+
|
77
|
+
id = rand(1000) while ids.include?(id)
|
78
|
+
break
|
79
|
+
end
|
80
|
+
|
81
|
+
data = {}
|
82
|
+
data['id'] = id
|
83
|
+
data['flavor'] = attr[:flavor]
|
84
|
+
data['name'] = attr[:name]
|
85
|
+
data['state'] = 'RUNNING'
|
86
|
+
data['status'] = 3
|
87
|
+
self.data['vms'] << data
|
88
|
+
data
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|