idrac 0.3.3 → 0.4.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/lib/idrac/client.rb +12 -2
- data/lib/idrac/power.rb +16 -16
- data/lib/idrac/system.rb +85 -85
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac/virtual_media.rb +12 -18
- data/lib/idrac.rb +18 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04a47e02d37a42167e7007aa92fc8f1d1b4023a52d44bb65ed1132a2035c110a
|
4
|
+
data.tar.gz: d17c640f8c8c238a6ea6aa4c037f39d2a9c2bfeaaebc478b1a464c75a648bbc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a64a9f69bf757578c9d3fb6d95aeb54b33b4a6604ba432a0b52cdca1e0fdbb2451b19191e2ebbe2468a025648a985a8e5122559f04063db7be8fb20b9a997759
|
7
|
+
data.tar.gz: 867beff66c1896abe949e87bf65b0c5df5ce65bda9d5e443f31a490792db7a63c737fe1295ea7a85c70a4f43af01ee1d6cc545b43939b581b76ade5b6cd641d1
|
data/lib/idrac/client.rb
CHANGED
@@ -286,6 +286,16 @@ module IDRAC
|
|
286
286
|
web.capture_screenshot
|
287
287
|
end
|
288
288
|
|
289
|
+
# Parse JSON string and convert to OpenStruct
|
290
|
+
# @param json [String] JSON string
|
291
|
+
# @param use_ostruct [Boolean] Whether to convert to OpenStruct (default: true)
|
292
|
+
# @return [OpenStruct, Hash] Parsed response
|
293
|
+
def parse_json(json, use_ostruct = true)
|
294
|
+
parsed = JSON.parse(json)
|
295
|
+
use_ostruct ? IDRAC::Util.to_ostruct(parsed) : parsed
|
296
|
+
end
|
297
|
+
|
298
|
+
# Kept for backward compatibility
|
289
299
|
def base_url
|
290
300
|
protocol = use_ssl ? 'https' : 'http'
|
291
301
|
"#{protocol}://#{host}:#{port}"
|
@@ -294,8 +304,8 @@ module IDRAC
|
|
294
304
|
def redfish_version
|
295
305
|
response = authenticated_request(:get, "/redfish/v1")
|
296
306
|
if response.status == 200
|
297
|
-
data =
|
298
|
-
data
|
307
|
+
data = parse_json(response.body)
|
308
|
+
data.RedfishVersion
|
299
309
|
else
|
300
310
|
raise Error, "Failed to get Redfish version: #{response.status} - #{response.body}"
|
301
311
|
end
|
data/lib/idrac/power.rb
CHANGED
@@ -30,9 +30,9 @@ module IDRAC
|
|
30
30
|
break
|
31
31
|
when 409
|
32
32
|
begin
|
33
|
-
error_data =
|
34
|
-
if error_data
|
35
|
-
error_data
|
33
|
+
error_data = parse_json(response.body)
|
34
|
+
if error_data.error && error_data.error['@Message.ExtendedInfo'] &&
|
35
|
+
error_data.error['@Message.ExtendedInfo'].any? { |m| m.Message =~ /Server is already powered ON/ }
|
36
36
|
puts "Server is already powered ON.".yellow
|
37
37
|
return false
|
38
38
|
else
|
@@ -84,9 +84,9 @@ module IDRAC
|
|
84
84
|
when 409
|
85
85
|
# Conflict -- Server is already off
|
86
86
|
begin
|
87
|
-
error_data =
|
88
|
-
if error_data
|
89
|
-
error_data
|
87
|
+
error_data = parse_json(response.body)
|
88
|
+
if error_data.error && error_data.error['@Message.ExtendedInfo'] &&
|
89
|
+
error_data.error['@Message.ExtendedInfo'].any? { |m| m.Message =~ /Server is already powered OFF/ }
|
90
90
|
puts "Server is already powered OFF.".yellow
|
91
91
|
return false
|
92
92
|
else
|
@@ -98,8 +98,8 @@ module IDRAC
|
|
98
98
|
else
|
99
99
|
error_message = "Failed to power off server. Status code: #{response.status}"
|
100
100
|
begin
|
101
|
-
error_data =
|
102
|
-
error_message += ", Message: #{error_data
|
101
|
+
error_data = parse_json(response.body)
|
102
|
+
error_message += ", Message: #{error_data.error.message}" if error_data.error && error_data.error.message
|
103
103
|
rescue
|
104
104
|
# Ignore JSON parsing errors
|
105
105
|
end
|
@@ -137,8 +137,8 @@ module IDRAC
|
|
137
137
|
else
|
138
138
|
error_message = "Failed to reboot server. Status code: #{response.status}"
|
139
139
|
begin
|
140
|
-
error_data =
|
141
|
-
error_message += ", Message: #{error_data
|
140
|
+
error_data = parse_json(response.body)
|
141
|
+
error_message += ", Message: #{error_data.error.message}" if error_data.error && error_data.error.message
|
142
142
|
rescue
|
143
143
|
# Ignore JSON parsing errors
|
144
144
|
end
|
@@ -156,8 +156,8 @@ module IDRAC
|
|
156
156
|
|
157
157
|
if response.status == 200
|
158
158
|
begin
|
159
|
-
system_data =
|
160
|
-
return system_data
|
159
|
+
system_data = parse_json(response.body)
|
160
|
+
return system_data.PowerState
|
161
161
|
rescue JSON::ParserError
|
162
162
|
raise Error, "Failed to parse power state response: #{response.body}"
|
163
163
|
end
|
@@ -174,8 +174,8 @@ module IDRAC
|
|
174
174
|
|
175
175
|
if response.status == 200
|
176
176
|
begin
|
177
|
-
data =
|
178
|
-
watts = data
|
177
|
+
data = parse_json(response.body)
|
178
|
+
watts = data.PowerControl[0].PowerConsumedWatts
|
179
179
|
# puts "Power usage: #{watts} watts".light_cyan
|
180
180
|
return watts.to_f
|
181
181
|
rescue JSON::ParserError
|
@@ -184,8 +184,8 @@ module IDRAC
|
|
184
184
|
else
|
185
185
|
error_message = "Failed to get power usage. Status code: #{response.status}"
|
186
186
|
begin
|
187
|
-
error_data =
|
188
|
-
error_message += ", Message: #{error_data
|
187
|
+
error_data = parse_json(response.body)
|
188
|
+
error_message += ", Message: #{error_data.error.message}" if error_data.error && error_data.error.message
|
189
189
|
rescue
|
190
190
|
# Ignore JSON parsing errors
|
191
191
|
end
|
data/lib/idrac/system.rb
CHANGED
@@ -9,21 +9,21 @@ module IDRAC
|
|
9
9
|
|
10
10
|
if response.status == 200
|
11
11
|
begin
|
12
|
-
data =
|
13
|
-
memory = data
|
14
|
-
dimm_name = m
|
12
|
+
data = parse_json(response.body)
|
13
|
+
memory = data.Members.map do |m|
|
14
|
+
dimm_name = m.Name # e.g. DIMM A1
|
15
15
|
bank, index = /DIMM ([A-Z])(\d+)/.match(dimm_name).captures rescue [nil, nil]
|
16
16
|
|
17
|
-
puts "DIMM: #{m
|
17
|
+
puts "DIMM: #{m.Model} #{m.Name} > #{m.CapacityMiB}MiB > #{m.Status.Health} > #{m.OperatingSpeedMhz}MHz > #{m.PartNumber} / #{m.SerialNumber}"
|
18
18
|
|
19
19
|
{
|
20
|
-
"model" => m
|
21
|
-
"name" => m
|
22
|
-
"capacity_bytes" => m
|
23
|
-
"health" => m
|
24
|
-
"speed_mhz" => m
|
25
|
-
"part_number" => m
|
26
|
-
"serial" => m
|
20
|
+
"model" => m.Model,
|
21
|
+
"name" => m.Name,
|
22
|
+
"capacity_bytes" => m.CapacityMiB.to_i.megabyte,
|
23
|
+
"health" => m.Status.Health ? m.Status.Health : "N/A",
|
24
|
+
"speed_mhz" => m.OperatingSpeedMhz,
|
25
|
+
"part_number" => m.PartNumber,
|
26
|
+
"serial" => m.SerialNumber,
|
27
27
|
"bank" => bank,
|
28
28
|
"index" => index.to_i
|
29
29
|
}
|
@@ -44,20 +44,20 @@ module IDRAC
|
|
44
44
|
|
45
45
|
if response.status == 200
|
46
46
|
begin
|
47
|
-
data =
|
47
|
+
data = parse_json(response.body)
|
48
48
|
puts "Power Supplies".green
|
49
49
|
|
50
|
-
psus = data
|
51
|
-
puts "PSU: #{psu
|
50
|
+
psus = data.PowerSupplies.map do |psu|
|
51
|
+
puts "PSU: #{psu.Name} > #{psu.PowerInputWatts}W > #{psu.Status.Health}"
|
52
52
|
{
|
53
|
-
"name" => psu
|
54
|
-
"voltage" => psu
|
55
|
-
"voltage_human" => psu
|
56
|
-
"watts" => psu
|
57
|
-
"part" => psu
|
58
|
-
"model" => psu
|
59
|
-
"serial" => psu
|
60
|
-
"status" => psu
|
53
|
+
"name" => psu.Name,
|
54
|
+
"voltage" => psu.LineInputVoltage,
|
55
|
+
"voltage_human" => psu.LineInputVoltageType, # AC240V
|
56
|
+
"watts" => psu.PowerInputWatts,
|
57
|
+
"part" => psu.PartNumber,
|
58
|
+
"model" => psu.Model,
|
59
|
+
"serial" => psu.SerialNumber,
|
60
|
+
"status" => psu.Status.Health,
|
61
61
|
}
|
62
62
|
end
|
63
63
|
|
@@ -80,15 +80,15 @@ module IDRAC
|
|
80
80
|
response = authenticated_request(:get, "/redfish/v1/Chassis/System.Embedded.1/Thermal?$expand=*($levels=1)")
|
81
81
|
|
82
82
|
if response.status == 200
|
83
|
-
data =
|
83
|
+
data = parse_json(response.body)
|
84
84
|
|
85
|
-
fans = data
|
86
|
-
puts "Fan: #{fan
|
85
|
+
fans = data.Fans.map do |fan|
|
86
|
+
puts "Fan: #{fan.Name} > #{fan.Reading} > #{fan.Status.Health}"
|
87
87
|
{
|
88
|
-
"name" => fan
|
89
|
-
"rpm" => fan
|
90
|
-
"serial" => fan
|
91
|
-
"status" => fan
|
88
|
+
"name" => fan.Name,
|
89
|
+
"rpm" => fan.Reading,
|
90
|
+
"serial" => fan.SerialNumber,
|
91
|
+
"status" => fan.Status.Health
|
92
92
|
}
|
93
93
|
end
|
94
94
|
|
@@ -96,7 +96,7 @@ module IDRAC
|
|
96
96
|
elsif response.status.between?(400, 499)
|
97
97
|
# Check if system is powered off
|
98
98
|
power_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=PowerState")
|
99
|
-
if power_response.status == 200 &&
|
99
|
+
if power_response.status == 200 && parse_json(power_response.body).PowerState == "Off"
|
100
100
|
puts "WARN: System is off. Fans are not available.".yellow
|
101
101
|
return []
|
102
102
|
end
|
@@ -120,12 +120,12 @@ module IDRAC
|
|
120
120
|
|
121
121
|
if response.status == 200
|
122
122
|
begin
|
123
|
-
adapters_data =
|
123
|
+
adapters_data = parse_json(response.body)
|
124
124
|
|
125
125
|
# Determine iDRAC version for different port paths
|
126
126
|
idrac_version_response = authenticated_request(:get, "/redfish/v1")
|
127
|
-
idrac_version_data =
|
128
|
-
server = idrac_version_data
|
127
|
+
idrac_version_data = parse_json(idrac_version_response.body)
|
128
|
+
server = idrac_version_data.RedfishVersion || idrac_version_response.headers["server"]
|
129
129
|
|
130
130
|
is_idrac9 = case server.to_s.downcase
|
131
131
|
when /idrac\/9/
|
@@ -141,32 +141,32 @@ module IDRAC
|
|
141
141
|
|
142
142
|
port_part = is_idrac9 ? 'Ports' : 'NetworkPorts'
|
143
143
|
|
144
|
-
nics = adapters_data
|
145
|
-
path = "#{adapter[
|
144
|
+
nics = adapters_data.Members.map do |adapter|
|
145
|
+
path = "#{adapter['@odata.id'].split("v1/").last}/#{port_part}?$expand=*($levels=1)"
|
146
146
|
ports_response = authenticated_request(:get, "/redfish/v1/#{path}")
|
147
147
|
|
148
148
|
if ports_response.status == 200
|
149
|
-
ports_data =
|
149
|
+
ports_data = parse_json(ports_response.body)
|
150
150
|
|
151
|
-
ports = ports_data
|
151
|
+
ports = ports_data.Members.map do |nic|
|
152
152
|
if is_idrac9
|
153
|
-
link_speed_mbps = nic
|
154
|
-
mac_addr = nic
|
155
|
-
port_num = nic
|
156
|
-
network_technology = nic
|
157
|
-
link_status = nic
|
153
|
+
link_speed_mbps = nic.CurrentSpeedGbps.to_i * 1000
|
154
|
+
mac_addr = nic.Ethernet.AssociatedMACAddresses.first
|
155
|
+
port_num = nic.PortId
|
156
|
+
network_technology = nic.LinkNetworkTechnology
|
157
|
+
link_status = nic.LinkStatus =~ /up/i ? "Up" : "Down"
|
158
158
|
else # iDRAC 8
|
159
|
-
link_speed_mbps = nic
|
160
|
-
mac_addr = nic
|
161
|
-
port_num = nic
|
162
|
-
network_technology = nic
|
163
|
-
link_status = nic
|
159
|
+
link_speed_mbps = nic.SupportedLinkCapabilities.first.LinkSpeedMbps
|
160
|
+
mac_addr = nic.AssociatedNetworkAddresses.first
|
161
|
+
port_num = nic.PhysicalPortNumber
|
162
|
+
network_technology = nic.SupportedLinkCapabilities.first.LinkNetworkTechnology
|
163
|
+
link_status = nic.LinkStatus
|
164
164
|
end
|
165
165
|
|
166
|
-
puts "NIC: #{nic
|
166
|
+
puts "NIC: #{nic.Id} > #{mac_addr} > #{link_status} > #{port_num} > #{link_speed_mbps}Mbps"
|
167
167
|
|
168
168
|
{
|
169
|
-
"name" => nic
|
169
|
+
"name" => nic.Id,
|
170
170
|
"status" => link_status,
|
171
171
|
"mac" => mac_addr,
|
172
172
|
"port" => port_num,
|
@@ -176,21 +176,21 @@ module IDRAC
|
|
176
176
|
end
|
177
177
|
|
178
178
|
{
|
179
|
-
"name" => adapter
|
180
|
-
"manufacturer" => adapter
|
181
|
-
"model" => adapter
|
182
|
-
"part_number" => adapter
|
183
|
-
"serial" => adapter
|
179
|
+
"name" => adapter.Id,
|
180
|
+
"manufacturer" => adapter.Manufacturer,
|
181
|
+
"model" => adapter.Model,
|
182
|
+
"part_number" => adapter.PartNumber,
|
183
|
+
"serial" => adapter.SerialNumber,
|
184
184
|
"ports" => ports
|
185
185
|
}
|
186
186
|
else
|
187
187
|
# Return adapter info without ports if we can't get port details
|
188
188
|
{
|
189
|
-
"name" => adapter
|
190
|
-
"manufacturer" => adapter
|
191
|
-
"model" => adapter
|
192
|
-
"part_number" => adapter
|
193
|
-
"serial" => adapter
|
189
|
+
"name" => adapter.Id,
|
190
|
+
"manufacturer" => adapter.Manufacturer,
|
191
|
+
"model" => adapter.Model,
|
192
|
+
"part_number" => adapter.PartNumber,
|
193
|
+
"serial" => adapter.SerialNumber,
|
194
194
|
"ports" => []
|
195
195
|
}
|
196
196
|
end
|
@@ -211,17 +211,17 @@ module IDRAC
|
|
211
211
|
|
212
212
|
if response.status == 200
|
213
213
|
begin
|
214
|
-
data =
|
214
|
+
data = parse_json(response.body)
|
215
215
|
|
216
216
|
idrac = {
|
217
|
-
"name" => data
|
218
|
-
"status" => data
|
219
|
-
"mac" => data
|
220
|
-
"mask" => data
|
221
|
-
"ipv4" => data
|
222
|
-
"origin" => data
|
217
|
+
"name" => data.Id,
|
218
|
+
"status" => data.Status.Health == 'OK' ? 'Up' : 'Down',
|
219
|
+
"mac" => data.MACAddress,
|
220
|
+
"mask" => data.IPv4Addresses.first.SubnetMask,
|
221
|
+
"ipv4" => data.IPv4Addresses.first.Address,
|
222
|
+
"origin" => data.IPv4Addresses.first.AddressOrigin, # DHCP or Static
|
223
223
|
"port" => nil,
|
224
|
-
"speed_mbps" => data
|
224
|
+
"speed_mbps" => data.SpeedMbps,
|
225
225
|
"kind" => "ethernet"
|
226
226
|
}
|
227
227
|
|
@@ -240,32 +240,32 @@ module IDRAC
|
|
240
240
|
|
241
241
|
if response.status == 200
|
242
242
|
begin
|
243
|
-
data =
|
243
|
+
data = parse_json(response.body)
|
244
244
|
|
245
|
-
pci = data
|
246
|
-
manufacturer = stub
|
245
|
+
pci = data.Members.map do |stub|
|
246
|
+
manufacturer = stub.Manufacturer
|
247
247
|
|
248
248
|
# Get PCIe function details if available
|
249
249
|
pcie_function = nil
|
250
|
-
if stub.dig(
|
251
|
-
pcie_function_path = stub.dig(
|
250
|
+
if stub.dig(:Links, :PCIeFunctions, 0, :'@odata.id')
|
251
|
+
pcie_function_path = stub.dig(:Links, :PCIeFunctions, 0, :'@odata.id').split("v1/").last
|
252
252
|
function_response = authenticated_request(:get, "/redfish/v1/#{pcie_function_path}")
|
253
253
|
|
254
254
|
if function_response.status == 200
|
255
|
-
pcie_function =
|
255
|
+
pcie_function = parse_json(function_response.body)
|
256
256
|
end
|
257
257
|
end
|
258
258
|
|
259
259
|
# Create device info with available data
|
260
260
|
device_info = {
|
261
|
-
device_class: pcie_function ? pcie_function
|
261
|
+
device_class: pcie_function ? pcie_function.DeviceClass : nil,
|
262
262
|
manufacturer: manufacturer,
|
263
|
-
name: stub
|
264
|
-
description: stub
|
265
|
-
id: pcie_function ? pcie_function
|
266
|
-
slot_type: pcie_function ? pcie_function.dig(
|
267
|
-
bus_width: pcie_function ? pcie_function.dig(
|
268
|
-
nic: pcie_function ? pcie_function.dig(
|
263
|
+
name: stub.Name,
|
264
|
+
description: stub.Description,
|
265
|
+
id: pcie_function ? pcie_function.Id : stub.Id,
|
266
|
+
slot_type: pcie_function ? pcie_function.dig(:Oem, :Dell, :DellPCIeFunction, :SlotType) : nil,
|
267
|
+
bus_width: pcie_function ? pcie_function.dig(:Oem, :Dell, :DellPCIeFunction, :DataBusWidth) : nil,
|
268
|
+
nic: pcie_function ? pcie_function.dig(:Links, :EthernetInterfaces, 0, :'@odata.id') : nil
|
269
269
|
}
|
270
270
|
|
271
271
|
puts "PCI Device: #{device_info[:name]} > #{device_info[:manufacturer]} > #{device_info[:device_class]} > #{device_info[:description]} > #{device_info[:id]}"
|
@@ -331,15 +331,15 @@ module IDRAC
|
|
331
331
|
|
332
332
|
if response.status == 200
|
333
333
|
begin
|
334
|
-
data =
|
334
|
+
data = parse_json(response.body)
|
335
335
|
|
336
|
-
logs = data
|
337
|
-
puts "#{log
|
336
|
+
logs = data.Members.map do |log|
|
337
|
+
puts "#{log.Id} : #{log.Created} : #{log.Message} : #{log.Severity}".yellow
|
338
338
|
log
|
339
339
|
end
|
340
340
|
|
341
341
|
# Sort by creation date, newest first
|
342
|
-
return logs.sort_by { |log| log
|
342
|
+
return logs.sort_by { |log| log.Created }.reverse
|
343
343
|
rescue JSON::ParserError
|
344
344
|
raise Error, "Failed to parse system event logs response: #{response.body}"
|
345
345
|
end
|
@@ -364,7 +364,7 @@ module IDRAC
|
|
364
364
|
error_message = "Failed to clear System Event Logs. Status code: #{response.status}"
|
365
365
|
|
366
366
|
begin
|
367
|
-
error_data =
|
367
|
+
error_data = parse_json(response.body, false)
|
368
368
|
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
369
369
|
rescue
|
370
370
|
# Ignore JSON parsing errors
|
data/lib/idrac/version.rb
CHANGED
data/lib/idrac/virtual_media.rb
CHANGED
@@ -9,31 +9,25 @@ module IDRAC
|
|
9
9
|
|
10
10
|
if response.status == 200
|
11
11
|
begin
|
12
|
-
data =
|
12
|
+
data = parse_json(response.body)
|
13
13
|
|
14
|
-
media = data
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
{
|
24
|
-
device: m["Id"],
|
25
|
-
inserted: m["Inserted"],
|
26
|
-
image: m["Image"] || m["ConnectedVia"],
|
27
|
-
action_path: action_path
|
14
|
+
media = data.Members.map do |m|
|
15
|
+
{
|
16
|
+
"id" => m.Id,
|
17
|
+
"name" => m.Name,
|
18
|
+
"media_types" => m.MediaTypes,
|
19
|
+
"connected" => m.Inserted,
|
20
|
+
"image" => m.Image,
|
21
|
+
"write_protected" => m.WriteProtected
|
28
22
|
}
|
29
23
|
end
|
30
24
|
|
31
25
|
return media
|
32
|
-
rescue
|
33
|
-
raise Error, "Failed to
|
26
|
+
rescue => e
|
27
|
+
raise Error, "Failed to get virtual media status: #{e.message}"
|
34
28
|
end
|
35
29
|
else
|
36
|
-
raise Error, "Failed to get virtual media. Status code: #{response.status}"
|
30
|
+
raise Error, "Failed to get virtual media status. Status code: #{response.status}"
|
37
31
|
end
|
38
32
|
end
|
39
33
|
|
data/lib/idrac.rb
CHANGED
@@ -7,6 +7,7 @@ require 'faraday/multipart'
|
|
7
7
|
require 'base64'
|
8
8
|
require 'uri'
|
9
9
|
require 'colorize'
|
10
|
+
require 'ostruct'
|
10
11
|
# If dev, required debug
|
11
12
|
require 'debug' if ENV['RUBY_ENV'] == 'development'
|
12
13
|
|
@@ -29,6 +30,23 @@ module IDRAC
|
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
33
|
+
# Utility methods for the IDRAC module
|
34
|
+
module Util
|
35
|
+
# Recursively converts a hash to an OpenStruct
|
36
|
+
# @param obj [Hash, Array, Object] The object to convert
|
37
|
+
# @return [OpenStruct, Array, Object] The converted object
|
38
|
+
def self.to_ostruct(obj)
|
39
|
+
case obj
|
40
|
+
when Hash
|
41
|
+
OpenStruct.new(obj.transform_values { |val| to_ostruct(val) })
|
42
|
+
when Array
|
43
|
+
obj.map { |item| to_ostruct(item) }
|
44
|
+
else
|
45
|
+
obj
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
32
50
|
class Error < StandardError; end
|
33
51
|
|
34
52
|
def self.new(options = {})
|