radfish-ami 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +87 -0
- data/lib/radfish/ami/version.rb +7 -0
- data/lib/radfish/ami.rb +4 -0
- data/lib/radfish/ami_adapter.rb +790 -0
- data/lib/radfish-ami.rb +10 -0
- data/radfish-ami.gemspec +32 -0
- metadata +124 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 348afbf5f47b34017b22b55684ae17457b46ed74d4f160c85e2c648b4ac30b55
|
|
4
|
+
data.tar.gz: 5538ed34280b652637c588280019bff2fd08cbc420854b63fe96d9d190e572c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1107faba3f94f2f40b5e4f21e2ecd2889715fa0806dca5b2503c6926206c6d80d0657aee05fccb2f6bdc270790e7a1a3550ed63f839178051f352abaee210a4d
|
|
7
|
+
data.tar.gz: 786c5c3e9fc08e8b52b9b7c866bdacb7e9ced148bdfb68a75eee0b5bd754731e5383e7343712fa9d0e79ba85fd8872885440be691395eb3b978db47a3648fc0b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jonathan Siegel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Radfish-AMI
|
|
2
|
+
|
|
3
|
+
AMI BMC adapter for the [Radfish](https://github.com/buildio/radfish) Redfish API client. Provides support for ASRockRack servers and other systems using AMI MegaRAC BMC firmware.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'radfish-ami'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install radfish-ami
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
The adapter is automatically registered when you require the gem:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'radfish-ami'
|
|
31
|
+
|
|
32
|
+
# Connect to an ASRockRack server
|
|
33
|
+
client = Radfish.connect(
|
|
34
|
+
host: 'bmc.example.com',
|
|
35
|
+
username: 'admin',
|
|
36
|
+
password: 'your-password',
|
|
37
|
+
vendor: 'ami' # or 'asrockrack'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Get system information
|
|
41
|
+
puts client.system_info
|
|
42
|
+
|
|
43
|
+
# Power operations
|
|
44
|
+
puts client.power_status # => "On"
|
|
45
|
+
client.power_restart(force: true)
|
|
46
|
+
|
|
47
|
+
# Virtual media
|
|
48
|
+
client.insert_virtual_media('http://example.com/boot.iso')
|
|
49
|
+
client.set_boot_override('Cd', persistent: false)
|
|
50
|
+
client.power_restart
|
|
51
|
+
|
|
52
|
+
# Thermal data
|
|
53
|
+
client.fans.each { |fan| puts "#{fan['Name']}: #{fan['Reading']} RPM" }
|
|
54
|
+
client.temperatures.each { |t| puts "#{t['Name']}: #{t['ReadingCelsius']}C" }
|
|
55
|
+
|
|
56
|
+
# Storage
|
|
57
|
+
client.storage_controllers.each do |controller|
|
|
58
|
+
puts "Controller: #{controller.name}"
|
|
59
|
+
client.drives(controller).each { |d| puts " Drive: #{d['Name']}" }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Disconnect
|
|
63
|
+
client.logout
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Supported Features
|
|
67
|
+
|
|
68
|
+
- **Power Management**: Power on, off, restart, cycle, status
|
|
69
|
+
- **System Information**: Make, model, serial, BIOS version, CPU, memory, NICs
|
|
70
|
+
- **Thermal Monitoring**: Fan speeds, temperatures
|
|
71
|
+
- **Power Monitoring**: PSU status, power consumption
|
|
72
|
+
- **Storage**: Controllers, drives, volumes
|
|
73
|
+
- **Virtual Media**: Insert/eject ISO images
|
|
74
|
+
- **Boot Configuration**: Set boot order, one-time boot overrides
|
|
75
|
+
- **Network Configuration**: BMC network settings
|
|
76
|
+
- **Account Management**: Create, delete, update user accounts
|
|
77
|
+
- **SEL Log**: Read and clear system event logs
|
|
78
|
+
- **Task/Job Management**: Monitor long-running operations
|
|
79
|
+
|
|
80
|
+
## Tested Hardware
|
|
81
|
+
|
|
82
|
+
- ASRockRack GENOAD8UD-2T/X550 (AMD EPYC)
|
|
83
|
+
- Other ASRockRack servers with AMI MegaRAC BMC
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT License - see LICENSE file.
|
data/lib/radfish/ami.rb
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radfish
|
|
4
|
+
class AmiAdapter < Core::BaseClient
|
|
5
|
+
include Core::Power
|
|
6
|
+
include Core::System
|
|
7
|
+
include Core::Storage
|
|
8
|
+
include Core::VirtualMedia
|
|
9
|
+
include Core::Boot
|
|
10
|
+
include Core::Jobs
|
|
11
|
+
include Core::Utility
|
|
12
|
+
include Core::Network
|
|
13
|
+
|
|
14
|
+
# AMI BMC uses "Self" as the default system/manager/chassis ID
|
|
15
|
+
SYSTEM_ID = "Self"
|
|
16
|
+
MANAGER_ID = "Self"
|
|
17
|
+
CHASSIS_ID = "Self"
|
|
18
|
+
|
|
19
|
+
def vendor
|
|
20
|
+
"ami"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Session management
|
|
24
|
+
def login
|
|
25
|
+
@session = Core::Session.new(self)
|
|
26
|
+
@session.create
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def logout
|
|
30
|
+
return true unless @session
|
|
31
|
+
result = @session.delete
|
|
32
|
+
@session = nil
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def authenticated_request(method, path, **options)
|
|
37
|
+
ensure_session!
|
|
38
|
+
|
|
39
|
+
headers = options[:headers] || {}
|
|
40
|
+
headers["X-Auth-Token"] = @session.x_auth_token
|
|
41
|
+
headers["Accept"] ||= "application/json"
|
|
42
|
+
headers["Content-Type"] ||= "application/json" if [:post, :put, :patch].include?(method)
|
|
43
|
+
headers["Host"] = host_header if host_header
|
|
44
|
+
|
|
45
|
+
options[:headers] = headers
|
|
46
|
+
|
|
47
|
+
case method
|
|
48
|
+
when :get
|
|
49
|
+
http_get(path, **options)
|
|
50
|
+
when :post
|
|
51
|
+
http_post(path, **options)
|
|
52
|
+
when :put
|
|
53
|
+
http_put(path, **options)
|
|
54
|
+
when :patch
|
|
55
|
+
http_patch(path, **options)
|
|
56
|
+
when :delete
|
|
57
|
+
http_delete(path, **options)
|
|
58
|
+
else
|
|
59
|
+
raise ArgumentError, "Unknown HTTP method: #{method}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Power management
|
|
64
|
+
def power_status
|
|
65
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}")
|
|
66
|
+
if response.status == 200
|
|
67
|
+
data = JSON.parse(response.body)
|
|
68
|
+
data["PowerState"]
|
|
69
|
+
else
|
|
70
|
+
raise Error, "Failed to get power status: #{response.status}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def power_on
|
|
75
|
+
perform_reset_action("On")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def power_off(force: true)
|
|
79
|
+
if force
|
|
80
|
+
perform_reset_action("ForceOff")
|
|
81
|
+
else
|
|
82
|
+
perform_reset_action("GracefulShutdown")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def power_restart(force: true)
|
|
87
|
+
if force
|
|
88
|
+
perform_reset_action("ForceRestart")
|
|
89
|
+
else
|
|
90
|
+
# Graceful restart: shutdown then power on
|
|
91
|
+
power_off(force: false)
|
|
92
|
+
sleep 5
|
|
93
|
+
power_on
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def power_cycle
|
|
98
|
+
perform_reset_action("PowerCycle")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def reset_type_allowed
|
|
102
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/ResetActionInfo")
|
|
103
|
+
if response.status == 200
|
|
104
|
+
data = JSON.parse(response.body)
|
|
105
|
+
params = data.dig("Parameters") || []
|
|
106
|
+
reset_param = params.find { |p| p["Name"] == "ResetType" }
|
|
107
|
+
reset_param&.dig("AllowableValues") || []
|
|
108
|
+
else
|
|
109
|
+
# Fallback to common AMI reset types
|
|
110
|
+
%w[On ForceOff ForceRestart GracefulShutdown PowerCycle]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# System information
|
|
115
|
+
def system_info
|
|
116
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}")
|
|
117
|
+
raise Error, "Failed to get system info: #{response.status}" unless response.status == 200
|
|
118
|
+
JSON.parse(response.body)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def service_tag
|
|
122
|
+
system_info["SerialNumber"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def make
|
|
126
|
+
info = system_info
|
|
127
|
+
info["Manufacturer"] || "ASRockRack"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def model
|
|
131
|
+
system_info["Model"]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def serial
|
|
135
|
+
system_info["SerialNumber"]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def cpus
|
|
139
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/Processors")
|
|
140
|
+
return [] unless response.status == 200
|
|
141
|
+
|
|
142
|
+
collection = JSON.parse(response.body)
|
|
143
|
+
members = collection["Members"] || []
|
|
144
|
+
|
|
145
|
+
members.map do |member|
|
|
146
|
+
cpu_response = authenticated_request(:get, member["@odata.id"])
|
|
147
|
+
next nil unless cpu_response.status == 200
|
|
148
|
+
JSON.parse(cpu_response.body)
|
|
149
|
+
end.compact
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def memory
|
|
153
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/Memory")
|
|
154
|
+
return [] unless response.status == 200
|
|
155
|
+
|
|
156
|
+
collection = JSON.parse(response.body)
|
|
157
|
+
members = collection["Members"] || []
|
|
158
|
+
|
|
159
|
+
members.map do |member|
|
|
160
|
+
mem_response = authenticated_request(:get, member["@odata.id"])
|
|
161
|
+
next nil unless mem_response.status == 200
|
|
162
|
+
JSON.parse(mem_response.body)
|
|
163
|
+
end.compact
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def nics
|
|
167
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/EthernetInterfaces")
|
|
168
|
+
return [] unless response.status == 200
|
|
169
|
+
|
|
170
|
+
collection = JSON.parse(response.body)
|
|
171
|
+
members = collection["Members"] || []
|
|
172
|
+
|
|
173
|
+
members.map do |member|
|
|
174
|
+
nic_response = authenticated_request(:get, member["@odata.id"])
|
|
175
|
+
next nil unless nic_response.status == 200
|
|
176
|
+
JSON.parse(nic_response.body)
|
|
177
|
+
end.compact
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def fans
|
|
181
|
+
thermal = get_thermal_data
|
|
182
|
+
thermal["Fans"] || []
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def temperatures
|
|
186
|
+
thermal = get_thermal_data
|
|
187
|
+
thermal["Temperatures"] || []
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def psus
|
|
191
|
+
power_data = get_power_data
|
|
192
|
+
power_data["PowerSupplies"] || []
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def power_consumption
|
|
196
|
+
power_data = get_power_data
|
|
197
|
+
power_data["PowerControl"]&.first || {}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def power_consumption_watts
|
|
201
|
+
consumption = power_consumption
|
|
202
|
+
consumption.dig("PowerMetrics", "AverageConsumedWatts") ||
|
|
203
|
+
consumption.dig("PowerMetrics", "CurConsumedWatts") ||
|
|
204
|
+
0
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Storage
|
|
208
|
+
def storage_controllers
|
|
209
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/Storage")
|
|
210
|
+
return [] unless response.status == 200
|
|
211
|
+
|
|
212
|
+
collection = JSON.parse(response.body)
|
|
213
|
+
members = collection["Members"] || []
|
|
214
|
+
|
|
215
|
+
members.map do |member|
|
|
216
|
+
controller_response = authenticated_request(:get, member["@odata.id"])
|
|
217
|
+
next nil unless controller_response.status == 200
|
|
218
|
+
data = JSON.parse(controller_response.body)
|
|
219
|
+
Radfish::Controller.new(
|
|
220
|
+
client: self,
|
|
221
|
+
id: data["Id"],
|
|
222
|
+
name: data["Name"],
|
|
223
|
+
model: data["Model"],
|
|
224
|
+
status: data.dig("Status", "Health"),
|
|
225
|
+
adapter_data: data
|
|
226
|
+
)
|
|
227
|
+
end.compact
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def drives(controller)
|
|
231
|
+
controller_id = controller.is_a?(Radfish::Controller) ? controller.id : controller
|
|
232
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/Storage/#{controller_id}")
|
|
233
|
+
return [] unless response.status == 200
|
|
234
|
+
|
|
235
|
+
data = JSON.parse(response.body)
|
|
236
|
+
drive_refs = data["Drives"] || []
|
|
237
|
+
|
|
238
|
+
drive_refs.map do |ref|
|
|
239
|
+
drive_response = authenticated_request(:get, ref["@odata.id"])
|
|
240
|
+
next nil unless drive_response.status == 200
|
|
241
|
+
JSON.parse(drive_response.body)
|
|
242
|
+
end.compact
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def volumes(controller)
|
|
246
|
+
controller_obj = controller.is_a?(Radfish::Controller) ? controller : nil
|
|
247
|
+
controller_id = controller.is_a?(Radfish::Controller) ? controller.id : controller
|
|
248
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/Storage/#{controller_id}/Volumes")
|
|
249
|
+
return [] unless response.status == 200
|
|
250
|
+
|
|
251
|
+
collection = JSON.parse(response.body)
|
|
252
|
+
members = collection["Members"] || []
|
|
253
|
+
|
|
254
|
+
members.map do |member|
|
|
255
|
+
volume_response = authenticated_request(:get, member["@odata.id"])
|
|
256
|
+
next nil unless volume_response.status == 200
|
|
257
|
+
data = JSON.parse(volume_response.body)
|
|
258
|
+
Radfish::Volume.new(
|
|
259
|
+
client: self,
|
|
260
|
+
controller: controller_obj,
|
|
261
|
+
id: data["Id"],
|
|
262
|
+
name: data["Name"],
|
|
263
|
+
capacity_bytes: data["CapacityBytes"],
|
|
264
|
+
raid_type: data["RAIDType"],
|
|
265
|
+
health: data.dig("Status", "Health"),
|
|
266
|
+
adapter_data: data
|
|
267
|
+
)
|
|
268
|
+
end.compact
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def volume_drives(volume)
|
|
272
|
+
volume_id = volume.is_a?(Radfish::Volume) ? volume.id : volume
|
|
273
|
+
# Get volume details to find linked drives
|
|
274
|
+
volume_data = volume.is_a?(Radfish::Volume) ? volume.adapter_data : nil
|
|
275
|
+
|
|
276
|
+
unless volume_data
|
|
277
|
+
# Need to fetch volume data
|
|
278
|
+
storage_controllers.each do |controller|
|
|
279
|
+
vols = volumes(controller)
|
|
280
|
+
vol = vols.find { |v| v.id == volume_id }
|
|
281
|
+
if vol
|
|
282
|
+
volume_data = vol.adapter_data
|
|
283
|
+
break
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
return [] unless volume_data
|
|
289
|
+
|
|
290
|
+
drive_refs = volume_data.dig("Links", "Drives") || []
|
|
291
|
+
drive_refs.map do |ref|
|
|
292
|
+
drive_response = authenticated_request(:get, ref["@odata.id"])
|
|
293
|
+
next nil unless drive_response.status == 200
|
|
294
|
+
JSON.parse(drive_response.body)
|
|
295
|
+
end.compact
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def storage_summary
|
|
299
|
+
controllers = storage_controllers
|
|
300
|
+
{
|
|
301
|
+
controller_count: controllers.size,
|
|
302
|
+
controllers: controllers.map do |c|
|
|
303
|
+
{
|
|
304
|
+
id: c.id,
|
|
305
|
+
name: c.name,
|
|
306
|
+
drive_count: drives(c).size,
|
|
307
|
+
volume_count: volumes(c).size
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Virtual Media
|
|
314
|
+
def virtual_media
|
|
315
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/#{MANAGER_ID}/VirtualMedia")
|
|
316
|
+
return [] unless response.status == 200
|
|
317
|
+
|
|
318
|
+
collection = JSON.parse(response.body)
|
|
319
|
+
members = collection["Members"] || []
|
|
320
|
+
|
|
321
|
+
members.map do |member|
|
|
322
|
+
vm_response = authenticated_request(:get, member["@odata.id"])
|
|
323
|
+
next nil unless vm_response.status == 200
|
|
324
|
+
JSON.parse(vm_response.body)
|
|
325
|
+
end.compact
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def virtual_media_status
|
|
329
|
+
virtual_media.map do |vm|
|
|
330
|
+
{
|
|
331
|
+
id: vm["Id"],
|
|
332
|
+
name: vm["Name"],
|
|
333
|
+
media_types: vm["MediaTypes"],
|
|
334
|
+
inserted: vm["Inserted"],
|
|
335
|
+
image: vm["Image"],
|
|
336
|
+
connected: vm["ConnectedVia"]
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def insert_virtual_media(iso_url, device: nil)
|
|
342
|
+
devices = virtual_media
|
|
343
|
+
target_device = if device
|
|
344
|
+
devices.find { |d| d["Id"] == device || d["Name"]&.include?(device.to_s) }
|
|
345
|
+
else
|
|
346
|
+
# Find first CD/DVD device
|
|
347
|
+
devices.find { |d| d["MediaTypes"]&.include?("CD") || d["MediaTypes"]&.include?("DVD") }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
raise VirtualMediaNotFoundError, "No suitable virtual media device found" unless target_device
|
|
351
|
+
|
|
352
|
+
device_path = target_device["@odata.id"]
|
|
353
|
+
actions = target_device.dig("Actions", "#VirtualMedia.InsertMedia")
|
|
354
|
+
|
|
355
|
+
if actions && actions["target"]
|
|
356
|
+
# Use the InsertMedia action
|
|
357
|
+
payload = { "Image" => iso_url, "Inserted" => true, "WriteProtected" => true }
|
|
358
|
+
response = authenticated_request(:post, actions["target"], body: payload.to_json)
|
|
359
|
+
else
|
|
360
|
+
# Fallback to PATCH method
|
|
361
|
+
payload = { "Image" => iso_url, "Inserted" => true, "WriteProtected" => true }
|
|
362
|
+
response = authenticated_request(:patch, device_path, body: payload.to_json)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
if response.status.between?(200, 204)
|
|
366
|
+
debug "Virtual media inserted successfully", 1, :green
|
|
367
|
+
true
|
|
368
|
+
else
|
|
369
|
+
error_msg = begin
|
|
370
|
+
JSON.parse(response.body).dig("error", "message")
|
|
371
|
+
rescue
|
|
372
|
+
response.body
|
|
373
|
+
end
|
|
374
|
+
raise VirtualMediaError, "Failed to insert virtual media: #{error_msg}"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def eject_virtual_media(device: nil)
|
|
379
|
+
devices = virtual_media
|
|
380
|
+
target_device = if device
|
|
381
|
+
devices.find { |d| d["Id"] == device || d["Name"]&.include?(device.to_s) }
|
|
382
|
+
else
|
|
383
|
+
# Find first mounted device
|
|
384
|
+
devices.find { |d| d["Inserted"] == true }
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
return true unless target_device && target_device["Inserted"]
|
|
388
|
+
|
|
389
|
+
device_path = target_device["@odata.id"]
|
|
390
|
+
actions = target_device.dig("Actions", "#VirtualMedia.EjectMedia")
|
|
391
|
+
|
|
392
|
+
if actions && actions["target"]
|
|
393
|
+
response = authenticated_request(:post, actions["target"], body: "{}".to_json)
|
|
394
|
+
else
|
|
395
|
+
payload = { "Image" => nil, "Inserted" => false }
|
|
396
|
+
response = authenticated_request(:patch, device_path, body: payload.to_json)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
response.status.between?(200, 204)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def unmount_all_media
|
|
403
|
+
virtual_media.each do |device|
|
|
404
|
+
next unless device["Inserted"]
|
|
405
|
+
eject_virtual_media(device: device["Id"])
|
|
406
|
+
end
|
|
407
|
+
true
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def mount_iso_and_boot(iso_url, device: nil)
|
|
411
|
+
insert_virtual_media(iso_url, device: device)
|
|
412
|
+
set_boot_override("Cd", persistent: false)
|
|
413
|
+
power_restart(force: true)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Boot configuration
|
|
417
|
+
def boot_config
|
|
418
|
+
info = system_info
|
|
419
|
+
info["Boot"] || {}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def set_boot_override(target, persistent: false)
|
|
423
|
+
enabled = persistent ? "Continuous" : "Once"
|
|
424
|
+
payload = {
|
|
425
|
+
"Boot" => {
|
|
426
|
+
"BootSourceOverrideTarget" => target,
|
|
427
|
+
"BootSourceOverrideEnabled" => enabled
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
response = authenticated_request(:patch, "/redfish/v1/Systems/#{SYSTEM_ID}", body: payload.to_json)
|
|
432
|
+
|
|
433
|
+
if response.status.between?(200, 204)
|
|
434
|
+
debug "Boot override set to #{target} (#{enabled})", 1, :green
|
|
435
|
+
true
|
|
436
|
+
else
|
|
437
|
+
error_msg = begin
|
|
438
|
+
JSON.parse(response.body).dig("error", "message")
|
|
439
|
+
rescue
|
|
440
|
+
response.body
|
|
441
|
+
end
|
|
442
|
+
raise Error, "Failed to set boot override: #{error_msg}"
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def clear_boot_override
|
|
447
|
+
payload = {
|
|
448
|
+
"Boot" => {
|
|
449
|
+
"BootSourceOverrideTarget" => "None",
|
|
450
|
+
"BootSourceOverrideEnabled" => "Disabled"
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
response = authenticated_request(:patch, "/redfish/v1/Systems/#{SYSTEM_ID}", body: payload.to_json)
|
|
455
|
+
response.status.between?(200, 204)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def set_boot_order(devices)
|
|
459
|
+
payload = {
|
|
460
|
+
"Boot" => {
|
|
461
|
+
"BootOrder" => devices
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
response = authenticated_request(:patch, "/redfish/v1/Systems/#{SYSTEM_ID}", body: payload.to_json)
|
|
466
|
+
response.status.between?(200, 204)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def get_boot_devices
|
|
470
|
+
boot_config["BootOrder"] || []
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def boot_to_pxe(persistent: false)
|
|
474
|
+
set_boot_override("Pxe", persistent: persistent)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def boot_to_disk(persistent: false)
|
|
478
|
+
set_boot_override("Hdd", persistent: persistent)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def boot_to_cd(persistent: false)
|
|
482
|
+
set_boot_override("Cd", persistent: persistent)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def boot_to_usb(persistent: false)
|
|
486
|
+
set_boot_override("Usb", persistent: persistent)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def boot_to_bios_setup(persistent: false)
|
|
490
|
+
set_boot_override("BiosSetup", persistent: persistent)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Jobs/Tasks
|
|
494
|
+
def jobs
|
|
495
|
+
response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks")
|
|
496
|
+
return [] unless response.status == 200
|
|
497
|
+
|
|
498
|
+
collection = JSON.parse(response.body)
|
|
499
|
+
members = collection["Members"] || []
|
|
500
|
+
|
|
501
|
+
members.map do |member|
|
|
502
|
+
task_response = authenticated_request(:get, member["@odata.id"])
|
|
503
|
+
next nil unless task_response.status == 200
|
|
504
|
+
JSON.parse(task_response.body)
|
|
505
|
+
end.compact
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def job_status(job_id)
|
|
509
|
+
response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}")
|
|
510
|
+
if response.status == 200
|
|
511
|
+
JSON.parse(response.body)
|
|
512
|
+
else
|
|
513
|
+
raise TaskError, "Failed to get task status: #{response.status}"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def wait_for_job(job_id, timeout: 600)
|
|
518
|
+
start_time = Time.now
|
|
519
|
+
loop do
|
|
520
|
+
status = job_status(job_id)
|
|
521
|
+
state = status["TaskState"]
|
|
522
|
+
|
|
523
|
+
case state
|
|
524
|
+
when "Completed"
|
|
525
|
+
return status
|
|
526
|
+
when "Exception", "Killed", "Cancelled"
|
|
527
|
+
raise TaskFailedError, "Task #{job_id} failed: #{status['Messages']}"
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
if Time.now - start_time > timeout
|
|
531
|
+
raise TaskTimeoutError, "Task #{job_id} timed out after #{timeout} seconds"
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
sleep 5
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def cancel_job(job_id)
|
|
539
|
+
response = authenticated_request(:delete, "/redfish/v1/TaskService/Tasks/#{job_id}")
|
|
540
|
+
response.status.between?(200, 204)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def clear_completed_jobs
|
|
544
|
+
jobs.each do |job|
|
|
545
|
+
next unless %w[Completed Exception Killed Cancelled].include?(job["TaskState"])
|
|
546
|
+
cancel_job(job["Id"])
|
|
547
|
+
end
|
|
548
|
+
true
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def jobs_summary
|
|
552
|
+
all_jobs = jobs
|
|
553
|
+
{
|
|
554
|
+
total: all_jobs.size,
|
|
555
|
+
running: all_jobs.count { |j| j["TaskState"] == "Running" },
|
|
556
|
+
completed: all_jobs.count { |j| j["TaskState"] == "Completed" },
|
|
557
|
+
failed: all_jobs.count { |j| %w[Exception Killed Cancelled].include?(j["TaskState"]) }
|
|
558
|
+
}
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Utility
|
|
562
|
+
def sel_log
|
|
563
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/#{SYSTEM_ID}/LogServices/Log1/Entries")
|
|
564
|
+
return [] unless response.status == 200
|
|
565
|
+
|
|
566
|
+
collection = JSON.parse(response.body)
|
|
567
|
+
collection["Members"] || []
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def clear_sel_log
|
|
571
|
+
response = authenticated_request(:post, "/redfish/v1/Systems/#{SYSTEM_ID}/LogServices/Log1/Actions/LogService.ClearLog", body: "{}".to_json)
|
|
572
|
+
response.status.between?(200, 204)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def sel_summary(limit: 10)
|
|
576
|
+
entries = sel_log.first(limit)
|
|
577
|
+
entries.map do |entry|
|
|
578
|
+
{
|
|
579
|
+
id: entry["Id"],
|
|
580
|
+
created: entry["Created"],
|
|
581
|
+
severity: entry["Severity"],
|
|
582
|
+
message: entry["Message"]
|
|
583
|
+
}
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def accounts
|
|
588
|
+
response = authenticated_request(:get, "/redfish/v1/AccountService/Accounts")
|
|
589
|
+
return [] unless response.status == 200
|
|
590
|
+
|
|
591
|
+
collection = JSON.parse(response.body)
|
|
592
|
+
members = collection["Members"] || []
|
|
593
|
+
|
|
594
|
+
members.map do |member|
|
|
595
|
+
account_response = authenticated_request(:get, member["@odata.id"])
|
|
596
|
+
next nil unless account_response.status == 200
|
|
597
|
+
JSON.parse(account_response.body)
|
|
598
|
+
end.compact
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def create_account(username:, password:, role: "Administrator")
|
|
602
|
+
payload = {
|
|
603
|
+
"UserName" => username,
|
|
604
|
+
"Password" => password,
|
|
605
|
+
"RoleId" => role
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
response = authenticated_request(:post, "/redfish/v1/AccountService/Accounts", body: payload.to_json)
|
|
609
|
+
|
|
610
|
+
if response.status == 201
|
|
611
|
+
JSON.parse(response.body)
|
|
612
|
+
else
|
|
613
|
+
error_msg = begin
|
|
614
|
+
JSON.parse(response.body).dig("error", "message")
|
|
615
|
+
rescue
|
|
616
|
+
response.body
|
|
617
|
+
end
|
|
618
|
+
raise Error, "Failed to create account: #{error_msg}"
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def delete_account(username)
|
|
623
|
+
account = accounts.find { |a| a["UserName"] == username }
|
|
624
|
+
raise NotFoundError, "Account not found: #{username}" unless account
|
|
625
|
+
|
|
626
|
+
response = authenticated_request(:delete, account["@odata.id"])
|
|
627
|
+
response.status.between?(200, 204)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def update_account_password(username:, new_password:)
|
|
631
|
+
account = accounts.find { |a| a["UserName"] == username }
|
|
632
|
+
raise NotFoundError, "Account not found: #{username}" unless account
|
|
633
|
+
|
|
634
|
+
payload = { "Password" => new_password }
|
|
635
|
+
response = authenticated_request(:patch, account["@odata.id"], body: payload.to_json)
|
|
636
|
+
response.status.between?(200, 204)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def sessions
|
|
640
|
+
response = authenticated_request(:get, "/redfish/v1/SessionService/Sessions")
|
|
641
|
+
return [] unless response.status == 200
|
|
642
|
+
|
|
643
|
+
collection = JSON.parse(response.body)
|
|
644
|
+
members = collection["Members"] || []
|
|
645
|
+
|
|
646
|
+
members.map do |member|
|
|
647
|
+
session_response = authenticated_request(:get, member["@odata.id"])
|
|
648
|
+
next nil unless session_response.status == 200
|
|
649
|
+
JSON.parse(session_response.body)
|
|
650
|
+
end.compact
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def service_info
|
|
654
|
+
service_root
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def get_firmware_version
|
|
658
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/#{MANAGER_ID}")
|
|
659
|
+
if response.status == 200
|
|
660
|
+
data = JSON.parse(response.body)
|
|
661
|
+
data["FirmwareVersion"]
|
|
662
|
+
else
|
|
663
|
+
nil
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Network configuration
|
|
668
|
+
def get_bmc_network
|
|
669
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/#{MANAGER_ID}/EthernetInterfaces")
|
|
670
|
+
return {} unless response.status == 200
|
|
671
|
+
|
|
672
|
+
collection = JSON.parse(response.body)
|
|
673
|
+
members = collection["Members"] || []
|
|
674
|
+
return {} if members.empty?
|
|
675
|
+
|
|
676
|
+
# Get the first interface (typically the main BMC interface)
|
|
677
|
+
interface_response = authenticated_request(:get, members.first["@odata.id"])
|
|
678
|
+
return {} unless interface_response.status == 200
|
|
679
|
+
|
|
680
|
+
data = JSON.parse(interface_response.body)
|
|
681
|
+
|
|
682
|
+
ipv4 = data.dig("IPv4Addresses", 0) || {}
|
|
683
|
+
dhcp_enabled = data.dig("DHCPv4", "DHCPEnabled")
|
|
684
|
+
{
|
|
685
|
+
"id" => data["Id"],
|
|
686
|
+
"hostname" => data["HostName"],
|
|
687
|
+
"fqdn" => data["FQDN"],
|
|
688
|
+
"mac_address" => data["MACAddress"],
|
|
689
|
+
"ipv4_address" => ipv4["Address"],
|
|
690
|
+
"subnet_mask" => ipv4["SubnetMask"],
|
|
691
|
+
"gateway" => ipv4["Gateway"],
|
|
692
|
+
"mode" => dhcp_enabled ? "DHCP" : "Static",
|
|
693
|
+
"address_origin" => ipv4["AddressOrigin"],
|
|
694
|
+
"dhcp_enabled" => dhcp_enabled,
|
|
695
|
+
"dns_servers" => data["NameServers"]
|
|
696
|
+
}
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def set_bmc_network(ip_address: nil, subnet_mask: nil, gateway: nil,
|
|
700
|
+
dns_primary: nil, dns_secondary: nil, hostname: nil,
|
|
701
|
+
dhcp: false)
|
|
702
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/#{MANAGER_ID}/EthernetInterfaces")
|
|
703
|
+
return false unless response.status == 200
|
|
704
|
+
|
|
705
|
+
collection = JSON.parse(response.body)
|
|
706
|
+
members = collection["Members"] || []
|
|
707
|
+
return false if members.empty?
|
|
708
|
+
|
|
709
|
+
interface_path = members.first["@odata.id"]
|
|
710
|
+
|
|
711
|
+
if dhcp
|
|
712
|
+
payload = {
|
|
713
|
+
"DHCPv4" => { "DHCPEnabled" => true }
|
|
714
|
+
}
|
|
715
|
+
else
|
|
716
|
+
payload = {}
|
|
717
|
+
|
|
718
|
+
if ip_address || subnet_mask || gateway
|
|
719
|
+
payload["IPv4Addresses"] = [{
|
|
720
|
+
"Address" => ip_address,
|
|
721
|
+
"SubnetMask" => subnet_mask,
|
|
722
|
+
"Gateway" => gateway
|
|
723
|
+
}.compact]
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
if dns_primary || dns_secondary
|
|
727
|
+
payload["NameServers"] = [dns_primary, dns_secondary].compact
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
payload["HostName"] = hostname if hostname
|
|
731
|
+
|
|
732
|
+
payload["DHCPv4"] = { "DHCPEnabled" => false }
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
response = authenticated_request(:patch, interface_path, body: payload.to_json)
|
|
736
|
+
response.status.between?(200, 204)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
private
|
|
740
|
+
|
|
741
|
+
def ensure_session!
|
|
742
|
+
unless @session&.x_auth_token
|
|
743
|
+
raise AuthenticationError, "Not logged in. Call #login first."
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def perform_reset_action(reset_type)
|
|
748
|
+
payload = { "ResetType" => reset_type }
|
|
749
|
+
response = authenticated_request(
|
|
750
|
+
:post,
|
|
751
|
+
"/redfish/v1/Systems/#{SYSTEM_ID}/Actions/ComputerSystem.Reset",
|
|
752
|
+
body: payload.to_json
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if response.status.between?(200, 204)
|
|
756
|
+
debug "Reset action #{reset_type} performed successfully", 1, :green
|
|
757
|
+
true
|
|
758
|
+
else
|
|
759
|
+
error_msg = begin
|
|
760
|
+
JSON.parse(response.body).dig("error", "message")
|
|
761
|
+
rescue
|
|
762
|
+
response.body
|
|
763
|
+
end
|
|
764
|
+
raise Error, "Failed to perform reset action #{reset_type}: #{error_msg}"
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def get_thermal_data
|
|
769
|
+
response = authenticated_request(:get, "/redfish/v1/Chassis/#{CHASSIS_ID}/Thermal")
|
|
770
|
+
if response.status == 200
|
|
771
|
+
JSON.parse(response.body)
|
|
772
|
+
else
|
|
773
|
+
{}
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def get_power_data
|
|
778
|
+
response = authenticated_request(:get, "/redfish/v1/Chassis/#{CHASSIS_ID}/Power")
|
|
779
|
+
if response.status == 200
|
|
780
|
+
JSON.parse(response.body)
|
|
781
|
+
else
|
|
782
|
+
{}
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# Register the AMI adapter
|
|
788
|
+
register_adapter("ami", AmiAdapter)
|
|
789
|
+
register_adapter("asrockrack", AmiAdapter)
|
|
790
|
+
end
|
data/lib/radfish-ami.rb
ADDED
data/radfish-ami.gemspec
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/radfish/ami/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "radfish-ami"
|
|
7
|
+
spec.version = Radfish::Ami::VERSION
|
|
8
|
+
spec.authors = ["Jonathan Siegel"]
|
|
9
|
+
spec.email = ["248302+usiegj00@users.noreply.github.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "AMI/ASRockRack adapter for Radfish"
|
|
12
|
+
spec.description = "AMI BMC adapter for Radfish Redfish API client. Provides support for ASRockRack servers and other systems using AMI MegaRAC BMC firmware."
|
|
13
|
+
spec.homepage = "https://github.com/buildio/radfish-ami"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
Dir["{lib}/**/*", "LICENSE", "README.md", "*.gemspec"].reject { |f| File.directory?(f) }
|
|
23
|
+
end
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.add_dependency "radfish", "~> 0.2"
|
|
27
|
+
|
|
28
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
29
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
31
|
+
spec.add_development_dependency "webmock", "~> 3.0"
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: radfish-ami
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jonathan Siegel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-20 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: radfish
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: webmock
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
description: AMI BMC adapter for Radfish Redfish API client. Provides support for
|
|
84
|
+
ASRockRack servers and other systems using AMI MegaRAC BMC firmware.
|
|
85
|
+
email:
|
|
86
|
+
- 248302+usiegj00@users.noreply.github.com
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- LICENSE
|
|
92
|
+
- README.md
|
|
93
|
+
- lib/radfish-ami.rb
|
|
94
|
+
- lib/radfish/ami.rb
|
|
95
|
+
- lib/radfish/ami/version.rb
|
|
96
|
+
- lib/radfish/ami_adapter.rb
|
|
97
|
+
- radfish-ami.gemspec
|
|
98
|
+
homepage: https://github.com/buildio/radfish-ami
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
homepage_uri: https://github.com/buildio/radfish-ami
|
|
103
|
+
source_code_uri: https://github.com/buildio/radfish-ami
|
|
104
|
+
changelog_uri: https://github.com/buildio/radfish-ami/blob/main/CHANGELOG.md
|
|
105
|
+
post_install_message:
|
|
106
|
+
rdoc_options: []
|
|
107
|
+
require_paths:
|
|
108
|
+
- lib
|
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: 3.1.0
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubygems_version: 3.5.22
|
|
121
|
+
signing_key:
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: AMI/ASRockRack adapter for Radfish
|
|
124
|
+
test_files: []
|