ibm_power_hmc 0.18.0 → 0.19.0
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 +4 -4
- data/CHANGELOG.md +76 -1
- data/README.md +7 -1
- data/lib/ibm_power_hmc/apis/connection.rb +197 -0
- data/lib/ibm_power_hmc/{job.rb → apis/job.rb} +0 -0
- data/lib/ibm_power_hmc/{pcm.rb → apis/pcm.rb} +0 -0
- data/lib/ibm_power_hmc/apis/sem.rb +20 -0
- data/lib/ibm_power_hmc/apis/templates.rb +171 -0
- data/lib/ibm_power_hmc/{connection.rb → apis/uom.rb} +39 -395
- data/lib/ibm_power_hmc/schema/parser.rb +232 -0
- data/lib/ibm_power_hmc/schema/pcm.rb +31 -0
- data/lib/ibm_power_hmc/schema/sem.rb +92 -0
- data/lib/ibm_power_hmc/schema/templates.rb +108 -0
- data/lib/ibm_power_hmc/{parser.rb → schema/uom.rb} +3 -358
- data/lib/ibm_power_hmc/version.rb +1 -1
- data/lib/ibm_power_hmc.rb +11 -4
- metadata +13 -6
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module IbmPowerHmc
|
7
|
+
##
|
8
|
+
# Generic parser for HMC K2 XML responses.
|
9
|
+
class Parser
|
10
|
+
def initialize(body)
|
11
|
+
@doc = REXML::Document.new(body)
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# @!method entry
|
16
|
+
# Return the first K2 entry element in the response.
|
17
|
+
# @return [REXML::Element, nil] The first entry element.
|
18
|
+
def entry
|
19
|
+
@doc.elements["entry"]
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# @!method object(filter_type = nil)
|
24
|
+
# Parse the first K2 entry element into an object.
|
25
|
+
# @param filter_type [String] Entry type must match the specified type.
|
26
|
+
# @return [IbmPowerHmc::AbstractRest, nil] The parsed object.
|
27
|
+
def object(filter_type = nil)
|
28
|
+
self.class.to_obj(entry, filter_type)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.to_obj(entry, filter_type = nil)
|
32
|
+
return if entry.nil?
|
33
|
+
|
34
|
+
content = entry.elements["content[@type]"]
|
35
|
+
return if content.nil?
|
36
|
+
|
37
|
+
type = content.attributes["type"].split("=")[1] || filter_type.to_s
|
38
|
+
return unless filter_type.nil? || filter_type.to_s == type
|
39
|
+
|
40
|
+
Module.const_get("IbmPowerHmc::#{type}").new(entry)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Parser for HMC K2 feeds.
|
46
|
+
# A feed encapsulates a list of entries like this:
|
47
|
+
# <feed>
|
48
|
+
# <entry>
|
49
|
+
# <!-- entry #1 -->
|
50
|
+
# </entry>
|
51
|
+
# <entry>
|
52
|
+
# <!-- entry #2 -->
|
53
|
+
# </entry>
|
54
|
+
# ...
|
55
|
+
# </feed>
|
56
|
+
class FeedParser < Parser
|
57
|
+
def entries
|
58
|
+
objs = []
|
59
|
+
@doc.each_element("feed/entry") do |entry|
|
60
|
+
objs << yield(entry)
|
61
|
+
end
|
62
|
+
objs
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# @!method objects(filter_type = nil)
|
67
|
+
# Parse feed entries into objects.
|
68
|
+
# @param filter_type [String] Filter entries based on content type.
|
69
|
+
# @return [Array<IbmPowerHmc::AbstractRest>] The list of objects.
|
70
|
+
def objects(filter_type = nil)
|
71
|
+
entries do |entry|
|
72
|
+
self.class.to_obj(entry, filter_type)
|
73
|
+
end.compact
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# HMC generic K2 non-REST object.
|
79
|
+
# @abstract
|
80
|
+
# @attr_reader [REXML::Document] xml The XML document representing this object.
|
81
|
+
class AbstractNonRest
|
82
|
+
ATTRS = {}.freeze
|
83
|
+
attr_reader :xml
|
84
|
+
|
85
|
+
def initialize(xml)
|
86
|
+
@xml = xml
|
87
|
+
self.class::ATTRS.each { |varname, xpath| define_attr(varname, xpath) }
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# @!method define_attr(varname, xpath)
|
92
|
+
# Define an instance variable using the text of an XML element as value.
|
93
|
+
# @param varname [String] The name of the instance variable.
|
94
|
+
# @param xpath [String] The XPath of the XML element containing the text.
|
95
|
+
def define_attr(varname, xpath)
|
96
|
+
value = singleton(xpath)
|
97
|
+
self.class.__send__(:attr_reader, varname)
|
98
|
+
self.class.__send__(:define_method, "#{varname}=") do |v|
|
99
|
+
if v.nil?
|
100
|
+
xml.elements.delete(xpath)
|
101
|
+
else
|
102
|
+
create_element(xpath) if xml.elements[xpath].nil?
|
103
|
+
xml.elements[xpath].text = v
|
104
|
+
end
|
105
|
+
instance_variable_set("@#{varname}", v)
|
106
|
+
end
|
107
|
+
instance_variable_set("@#{varname}", value)
|
108
|
+
end
|
109
|
+
private :define_attr
|
110
|
+
|
111
|
+
##
|
112
|
+
# @!method create_element(xpath)
|
113
|
+
# Create a new XML element.
|
114
|
+
# @param xpath [String] The XPath of the XML element to create.
|
115
|
+
def create_element(xpath)
|
116
|
+
cur = xml
|
117
|
+
xpath.split("/").each do |el|
|
118
|
+
p = cur.elements[el]
|
119
|
+
if p.nil?
|
120
|
+
cur = cur.add_element(el)
|
121
|
+
else
|
122
|
+
cur = p
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# @!method singleton(xpath, attr = nil)
|
129
|
+
# Get the text (or the value of a specified attribute) of an XML element.
|
130
|
+
# @param xpath [String] The XPath of the XML element.
|
131
|
+
# @param attr [String] The name of the attribute.
|
132
|
+
# @return [String, nil] The text or attribute value of the XML element or nil.
|
133
|
+
# @example lpar.singleton("PartitionProcessorConfiguration/*/MaximumVirtualProcessors").to_i
|
134
|
+
def singleton(xpath, attr = nil)
|
135
|
+
elem = xml.elements[xpath]
|
136
|
+
return if elem.nil?
|
137
|
+
|
138
|
+
attr.nil? ? elem.text&.strip : elem.attributes[attr]
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_s
|
142
|
+
str = +"#{self.class.name}:\n"
|
143
|
+
self.class::ATTRS.each do |varname, _|
|
144
|
+
value = instance_variable_get("@#{varname}")
|
145
|
+
value = value.nil? ? "null" : "'#{value}'"
|
146
|
+
str << " #{varname}: #{value}\n"
|
147
|
+
end
|
148
|
+
str
|
149
|
+
end
|
150
|
+
|
151
|
+
def uuid_from_href(href, index = -1)
|
152
|
+
URI(href).path.split('/')[index]
|
153
|
+
end
|
154
|
+
|
155
|
+
def uuids_from_links(elem, index = -1)
|
156
|
+
xml.get_elements("#{elem}/link[@href]").map do |link|
|
157
|
+
uuid_from_href(link.attributes["href"], index)
|
158
|
+
end.compact
|
159
|
+
end
|
160
|
+
|
161
|
+
def timestamp(xpath)
|
162
|
+
# XML element containing a number of milliseconds since the Epoch.
|
163
|
+
Time.at(0, singleton(xpath).to_i, :millisecond).utc
|
164
|
+
end
|
165
|
+
|
166
|
+
def collection_of(name, type)
|
167
|
+
xml.get_elements([name, type].compact.join("/")).map do |elem|
|
168
|
+
Module.const_get("IbmPowerHmc::#{elem.name}").new(elem)
|
169
|
+
rescue NameError
|
170
|
+
nil
|
171
|
+
end.compact
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# HMC generic K2 REST object.
|
177
|
+
# Encapsulate data for a single REST object.
|
178
|
+
# The XML looks like this:
|
179
|
+
# <entry>
|
180
|
+
# <id>uuid</id>
|
181
|
+
# <published>timestamp</published>
|
182
|
+
# <link rel="SELF" href="https://..."/>
|
183
|
+
# <etag:etag>ETag</etag:etag>
|
184
|
+
# <content type="type">
|
185
|
+
# <!-- actual content here -->
|
186
|
+
# </content>
|
187
|
+
# </entry>
|
188
|
+
#
|
189
|
+
# @abstract
|
190
|
+
# @attr_reader [String] uuid The UUID of the object contained in the entry.
|
191
|
+
# @attr_reader [Time] published The time at which the entry was published.
|
192
|
+
# @attr_reader [URI::HTTPS] href The URL of the object itself.
|
193
|
+
# @attr_reader [String] etag The entity tag of the entry.
|
194
|
+
# @attr_reader [String] content_type The content type of the object contained in the entry.
|
195
|
+
class AbstractRest < AbstractNonRest
|
196
|
+
attr_reader :uuid, :published, :href, :etag, :content_type
|
197
|
+
|
198
|
+
def initialize(entry)
|
199
|
+
if entry.name != "entry"
|
200
|
+
# We are inlined.
|
201
|
+
super(entry)
|
202
|
+
return
|
203
|
+
end
|
204
|
+
|
205
|
+
@uuid = entry.elements["id"]&.text
|
206
|
+
@published = Time.xmlschema(entry.elements["published"]&.text)
|
207
|
+
link = entry.elements["link[@rel='SELF']"]
|
208
|
+
@href = URI(link.attributes["href"]) unless link.nil?
|
209
|
+
@etag = entry.elements["etag:etag"]&.text&.strip
|
210
|
+
content = entry.elements["content"]
|
211
|
+
@content_type = content.attributes["type"]
|
212
|
+
super(content.elements.first)
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_s
|
216
|
+
str = super
|
217
|
+
str << " uuid: '#{uuid}'\n" if defined?(@uuid)
|
218
|
+
str << " published: '#{published}'\n" if defined?(@published)
|
219
|
+
str
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Error response from HMC
|
224
|
+
class HttpErrorResponse < AbstractRest
|
225
|
+
ATTRS = {
|
226
|
+
:status => "HTTPStatus",
|
227
|
+
:uri => "RequestURI",
|
228
|
+
:reason => "ReasonCode",
|
229
|
+
:message => "Message"
|
230
|
+
}.freeze
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IbmPowerHmc
|
4
|
+
# Performance and Capacity Monitoring preferences
|
5
|
+
class ManagementConsolePcmPreference < AbstractRest
|
6
|
+
ATTRS = {
|
7
|
+
:max_ltm => "MaximumManagedSystemsForLongTermMonitor",
|
8
|
+
:max_compute_ltm => "MaximumManagedSystemsForComputeLTM",
|
9
|
+
:max_aggregation => "MaximumManagedSystemsForAggregation",
|
10
|
+
:max_stm => "MaximumManagedSystemsForShortTermMonitor",
|
11
|
+
:max_em => "MaximumManagedSystemsForEnergyMonitor",
|
12
|
+
:aggregated_storage_duration => "AggregatedMetricsStorageDuration"
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def managed_system_preferences
|
16
|
+
collection_of(nil, "ManagedSystemPcmPreference")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ManagedSystemPcmPreference < AbstractNonRest
|
21
|
+
ATTRS = {
|
22
|
+
:id => "Metadata/Atom/AtomID",
|
23
|
+
:name => "SystemName",
|
24
|
+
:long_term_monitor => "LongTermMonitorEnabled",
|
25
|
+
:aggregation => "AggregationEnabled",
|
26
|
+
:short_term_monitor => "ShortTermMonitorEnabled",
|
27
|
+
:compute_ltm => "ComputeLTMEnabled",
|
28
|
+
:energy_monitor => "EnergyMonitorEnabled"
|
29
|
+
}.freeze
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IbmPowerHmc
|
4
|
+
# Serviceable Event
|
5
|
+
class ServiceableEvent < AbstractRest
|
6
|
+
ATTRS = {
|
7
|
+
:prob_uuid => "problemUuid",
|
8
|
+
:hostname => "reportingConsoleNode/hostName",
|
9
|
+
:number => "problemNumber",
|
10
|
+
:hw_record => "problemManagementHardwareRecord",
|
11
|
+
:description => "shortDescription",
|
12
|
+
:state => "problemState",
|
13
|
+
:approval_state => "ApprovalState",
|
14
|
+
:refcode => "referenceCode",
|
15
|
+
:refcode_ext => "referenceCodeExtension",
|
16
|
+
:refcode_sys => "systemReferenceCode",
|
17
|
+
:call_home => "callHomeEnabled",
|
18
|
+
:dup_count => "duplicateCount",
|
19
|
+
:severity => "eventSeverity",
|
20
|
+
:notif_type => "notificationType",
|
21
|
+
:notif_status => "notificationStatus",
|
22
|
+
:post_action => "postAction",
|
23
|
+
:symptom => "symptomString",
|
24
|
+
:lpar_id => "partitionId",
|
25
|
+
:lpar_name => "partitionName",
|
26
|
+
:lpar_hostname => "partitionHostName",
|
27
|
+
:lpar_ostype => "partitionOSType",
|
28
|
+
:syslog_id => "sysLogId",
|
29
|
+
:total_events => "totalEvents"
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
def reporting_mtms
|
33
|
+
mtms("reportingManagedSystemNode")
|
34
|
+
end
|
35
|
+
|
36
|
+
def failing_mtms
|
37
|
+
mtms("failingManagedSystemNode")
|
38
|
+
end
|
39
|
+
|
40
|
+
def time
|
41
|
+
timestamp("primaryTimestamp")
|
42
|
+
end
|
43
|
+
|
44
|
+
def created_time
|
45
|
+
timestamp("createdTimestamp")
|
46
|
+
end
|
47
|
+
|
48
|
+
def first_reported_time
|
49
|
+
timestamp("firstReportedTimestamp")
|
50
|
+
end
|
51
|
+
|
52
|
+
def last_reported_time
|
53
|
+
timestamp("lastReportedTimestamp")
|
54
|
+
end
|
55
|
+
|
56
|
+
def frus
|
57
|
+
collection_of("fieldReplaceableUnits", "FieldReplaceableUnit")
|
58
|
+
end
|
59
|
+
|
60
|
+
def ext_files
|
61
|
+
collection_of("extendedErrorData", "ExtendedFileData")
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def mtms(prefix)
|
67
|
+
machtype = singleton("#{prefix}/managedTypeModelSerial/MachineType")
|
68
|
+
model = singleton("#{prefix}/managedTypeModelSerial/Model")
|
69
|
+
serial = singleton("#{prefix}/managedTypeModelSerial/SerialNumber")
|
70
|
+
"#{machtype}-#{model}*#{serial}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class FieldReplaceableUnit < AbstractNonRest
|
75
|
+
ATTRS = {
|
76
|
+
:part_number => "partNumber",
|
77
|
+
:fru_class => "class",
|
78
|
+
:description => "fieldReplaceableUnitDescription",
|
79
|
+
:location => "locationCode",
|
80
|
+
:serial => "SerialNumber",
|
81
|
+
:ccin => "ccin"
|
82
|
+
}.freeze
|
83
|
+
end
|
84
|
+
|
85
|
+
class ExtendedFileData < AbstractNonRest
|
86
|
+
ATTRS = {
|
87
|
+
:filename => "fileName",
|
88
|
+
:description => "description",
|
89
|
+
:zipfilename => "zipFileName"
|
90
|
+
}.freeze
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IbmPowerHmc
|
4
|
+
class PartitionTemplateSummary < AbstractRest
|
5
|
+
ATTRS = {
|
6
|
+
:name => "partitionTemplateName"
|
7
|
+
}.freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
class PartitionTemplate < AbstractRest
|
11
|
+
ATTRS = {
|
12
|
+
:name => "partitionTemplateName",
|
13
|
+
:description => "description",
|
14
|
+
:lpar_name => "logicalPartitionConfig/partitionName",
|
15
|
+
:lpar_type => "logicalPartitionConfig/partitionType",
|
16
|
+
:lpar_id => "logicalPartitionConfig/partitionId",
|
17
|
+
:os => "logicalPartitionConfig/osVersion",
|
18
|
+
:memory => "logicalPartitionConfig/memoryConfiguration/currMemory",
|
19
|
+
:dedicated => "logicalPartitionConfig/processorConfiguration/hasDedicatedProcessors",
|
20
|
+
:sharing_mode => "logicalPartitionConfig/processorConfiguration/sharingMode",
|
21
|
+
:vprocs => "logicalPartitionConfig/processorConfiguration/sharedProcessorConfiguration/desiredVirtualProcessors",
|
22
|
+
:proc_units => "logicalPartitionConfig/processorConfiguration/sharedProcessorConfiguration/desiredProcessingUnits",
|
23
|
+
:pool_id => "logicalPartitionConfig/processorConfiguration/sharedProcessorConfiguration/sharedProcessorPoolId",
|
24
|
+
:procs => "logicalPartitionConfig/processorConfiguration/dedicatedProcessorConfiguration/desiredProcessors"
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def vscsi
|
28
|
+
REXML::XPath.match(xml, 'logicalPartitionConfig/virtualSCSIClientAdapters/VirtualSCSIClientAdapter').map do |adap|
|
29
|
+
{
|
30
|
+
:vios => adap.elements['connectingPartitionName']&.text,
|
31
|
+
:physvol => adap.elements['associatedPhysicalVolume/PhysicalVolume/name']&.text,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def vscsi=(list = [])
|
37
|
+
adaps = REXML::Element.new('virtualSCSIClientAdapters')
|
38
|
+
adaps.add_attribute('schemaVersion', 'V1_5_0')
|
39
|
+
list.each do |vscsi|
|
40
|
+
adaps.add_element('VirtualSCSIClientAdapter', {'schemaVersion' => 'V1_5_0'}).tap do |v|
|
41
|
+
v.add_element('associatedLogicalUnits', {'schemaVersion' => 'V1_5_0'})
|
42
|
+
v.add_element('associatedPhysicalVolume', {'schemaVersion' => 'V1_5_0'}).tap do |e|
|
43
|
+
e.add_element('PhysicalVolume', {'schemaVersion' => 'V1_5_0'}).add_element('name').text = vscsi[:physvol] if vscsi[:physvol]
|
44
|
+
end
|
45
|
+
v.add_element('connectingPartitionName').text = vscsi[:vios]
|
46
|
+
v.add_element('AssociatedTargetDevices', {'schemaVersion' => 'V1_5_0'})
|
47
|
+
v.add_element('associatedVirtualOpticalMedia', {'schemaVersion' => 'V1_5_0'})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
if xml.elements['logicalPartitionConfig/virtualSCSIClientAdapters']
|
51
|
+
xml.elements['logicalPartitionConfig/virtualSCSIClientAdapters'] = adaps
|
52
|
+
else
|
53
|
+
xml.elements['logicalPartitionConfig'].add_element(adaps)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def vfc
|
58
|
+
REXML::XPath.match(xml, 'logicalPartitionConfig/virtualFibreChannelClientAdapters/VirtualFibreChannelClientAdapter').map do |adap|
|
59
|
+
{
|
60
|
+
:vios => adap.elements['connectingPartitionName']&.text,
|
61
|
+
:port => adap.elements['portName']&.text
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def vfc=(list = [])
|
67
|
+
adaps = REXML::Element.new('virtualFibreChannelClientAdapters')
|
68
|
+
adaps.add_attribute('schemaVersion', 'V1_5_0')
|
69
|
+
list.each do |vfc|
|
70
|
+
adaps.add_element('VirtualFibreChannelClientAdapter', {'schemaVersion' => 'V1_5_0'}).tap do |v|
|
71
|
+
v.add_element('connectingPartitionName').text = vfc[:vios]
|
72
|
+
v.add_element('portName').text = vfc[:port]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
if xml.elements['logicalPartitionConfig/virtualFibreChannelClientAdapters']
|
76
|
+
xml.elements['logicalPartitionConfig/virtualFibreChannelClientAdapters'] = adaps
|
77
|
+
else
|
78
|
+
xml.elements['logicalPartitionConfig'].add_element(adaps)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def vlans
|
83
|
+
REXML::XPath.match(xml, 'logicalPartitionConfig/clientNetworkAdapters/ClientNetworkAdapter/clientVirtualNetworks/ClientVirtualNetwork').map do |vlan|
|
84
|
+
{
|
85
|
+
:name => vlan.elements['name']&.text,
|
86
|
+
:vlan_id => vlan.elements['vlanId']&.text,
|
87
|
+
:switch => vlan.elements['associatedSwitchName']&.text
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def vlans=(list = [])
|
93
|
+
adaps = REXML::Element.new('clientNetworkAdapters')
|
94
|
+
adaps.add_attribute('schemaVersion', 'V1_5_0')
|
95
|
+
list.each do |vlan|
|
96
|
+
adaps.add_element('ClientNetworkAdapter', {'schemaVersion' => 'V1_5_0'})
|
97
|
+
.add_element('clientVirtualNetworks', {'schemaVersion' => 'V1_5_0'})
|
98
|
+
.add_element('ClientVirtualNetwork', {'schemaVersion' => 'V1_5_0'})
|
99
|
+
.tap do |v|
|
100
|
+
v.add_element('name').text = vlan[:name]
|
101
|
+
v.add_element('vlanId').text = vlan[:vlan_id]
|
102
|
+
v.add_element('associatedSwitchName').text = vlan[:switch]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
xml.elements['logicalPartitionConfig/clientNetworkAdapters'] = adaps
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|