radfish 0.1.3 → 0.1.5
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/README.md +10 -0
- data/lib/radfish/bmc_info.rb +74 -0
- data/lib/radfish/cli.rb +58 -0
- data/lib/radfish/client.rb +28 -1
- data/lib/radfish/core/base_client.rb +49 -17
- data/lib/radfish/core/boot.rb +2 -2
- data/lib/radfish/core/network.rb +22 -0
- data/lib/radfish/http_client.rb +147 -0
- data/lib/radfish/pci_info.rb +37 -0
- data/lib/radfish/power_info.rb +106 -0
- data/lib/radfish/system_info.rb +74 -0
- data/lib/radfish/thermal_info.rb +26 -0
- data/lib/radfish/vendor_detector.rb +59 -60
- data/lib/radfish/version.rb +1 -1
- data/lib/radfish.rb +32 -13
- data/radfish.gemspec +1 -1
- metadata +18 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b58ebe70659a5db047e1d6efb8c8ec93ba7a7cce19f500f46204add014095566
|
4
|
+
data.tar.gz: 754e9d84d9bc6232cadaa4daf75fa4053b3d0fa9d53918b2f1160223775c85f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4653a3eb7d252f6a597db65f4a403f1b5f958dc5659d9a0a0f4eb73f669acc9b3be6b0d61b2686e4666823769e4027cd6eb31429a29c130cf08ab9b28aa64b77
|
7
|
+
data.tar.gz: dc6f9ffc5dd25d59ee68d70ab0071378d3d6aa4c4004f13e612d31415c817bdf48bc70433f7344685fb34ac529397226883d9e24ecfb66e3ccfbb1b6f52d7307
|
data/README.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
A Ruby client library that provides a unified interface for managing servers via Redfish API, with automatic vendor detection and adaptation.
|
4
4
|
|
5
|
+
## Data Normalization
|
6
|
+
|
7
|
+
Radfish takes a minimal approach to data normalization to ensure consistency across different systems and avoid mismatches from varying formats:
|
8
|
+
|
9
|
+
- **Manufacturer/Make**: Returns simplified vendor names (e.g., "Dell" instead of "Dell Inc.", "Supermicro" instead of "Super Micro Computer")
|
10
|
+
- **Model**: Returns core model identifiers without marketing prefixes (e.g., "R640" instead of "PowerEdge R640")
|
11
|
+
- **Consistent Fields**: All adapters normalize their data to use the same field names and formats
|
12
|
+
|
13
|
+
This approach minimizes potential mismatches across older systems or spacing/hyphenation issues with multi-token descriptors.
|
14
|
+
|
5
15
|
## Architecture
|
6
16
|
|
7
17
|
Radfish provides a vendor-agnostic interface for server management through Redfish, automatically detecting and adapting to different hardware vendors. The architecture consists of:
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radfish
|
4
|
+
class BmcInfo
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
@cache = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def keys
|
13
|
+
[:license_version, :firmware_version, :redfish_version, :mac_address, :ip_address, :hostname, :health]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
keys.each_with_object({}) do |key, hash|
|
18
|
+
hash[key] = send(key)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def license_version
|
23
|
+
fetch_bmc_info[:license_version] || fetch_bmc_info[:bmc_license_version]
|
24
|
+
end
|
25
|
+
|
26
|
+
def firmware_version
|
27
|
+
fetch_bmc_info[:firmware_version] || fetch_bmc_info[:bmc_firmware_version]
|
28
|
+
end
|
29
|
+
|
30
|
+
def redfish_version
|
31
|
+
fetch_bmc_info[:redfish_version]
|
32
|
+
end
|
33
|
+
|
34
|
+
def mac_address
|
35
|
+
fetch_bmc_info[:mac_address] || fetch_bmc_info[:bmc_mac_address]
|
36
|
+
end
|
37
|
+
|
38
|
+
def ip_address
|
39
|
+
fetch_bmc_info[:ip_address] || fetch_bmc_info[:bmc_ip_address]
|
40
|
+
end
|
41
|
+
|
42
|
+
def hostname
|
43
|
+
fetch_bmc_info[:hostname] || fetch_bmc_info[:bmc_hostname]
|
44
|
+
end
|
45
|
+
|
46
|
+
def health
|
47
|
+
fetch_bmc_info[:health] || fetch_bmc_info[:bmc_health]
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def fetch_bmc_info
|
53
|
+
@cache[:bmc_info] ||= begin
|
54
|
+
if @client.adapter.respond_to?(:bmc_info)
|
55
|
+
@client.adapter.bmc_info
|
56
|
+
elsif @client.adapter.respond_to?(:system_info)
|
57
|
+
# Extract BMC-related info from system_info if no dedicated method
|
58
|
+
info = @client.adapter.system_info
|
59
|
+
{
|
60
|
+
firmware_version: info[:bmc_firmware_version] || info[:firmware_version],
|
61
|
+
license_version: info[:bmc_license_version] || info[:license_version],
|
62
|
+
redfish_version: info[:redfish_version],
|
63
|
+
mac_address: info[:bmc_mac_address],
|
64
|
+
ip_address: info[:bmc_ip_address],
|
65
|
+
hostname: info[:bmc_hostname],
|
66
|
+
health: info[:bmc_health]
|
67
|
+
}
|
68
|
+
else
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/radfish/cli.rb
CHANGED
@@ -377,6 +377,64 @@ module Radfish
|
|
377
377
|
end
|
378
378
|
end
|
379
379
|
|
380
|
+
# Network Commands
|
381
|
+
desc "network SUBCOMMAND", "BMC network configuration"
|
382
|
+
option :ip, desc: "IP address to set"
|
383
|
+
option :mask, desc: "Subnet mask"
|
384
|
+
option :gateway, desc: "Gateway address"
|
385
|
+
option :dns1, desc: "Primary DNS server"
|
386
|
+
option :dns2, desc: "Secondary DNS server"
|
387
|
+
option :hostname, desc: "BMC hostname"
|
388
|
+
def network(subcommand = 'show')
|
389
|
+
with_client do |client|
|
390
|
+
case subcommand
|
391
|
+
when 'show', 'get', 'status'
|
392
|
+
data = client.get_bmc_network
|
393
|
+
if options[:json]
|
394
|
+
puts JSON.pretty_generate(data)
|
395
|
+
else
|
396
|
+
puts "=== BMC Network Configuration ===".green
|
397
|
+
puts "IP Address: #{data['ipv4_address']}".cyan
|
398
|
+
puts "Subnet Mask: #{data['subnet_mask']}".cyan
|
399
|
+
puts "Gateway: #{data['gateway']}".cyan
|
400
|
+
puts "Mode: #{data['mode']}".cyan
|
401
|
+
puts "MAC Address: #{data['mac_address']}".cyan
|
402
|
+
puts "Hostname: #{data['hostname']}".cyan if data['hostname']
|
403
|
+
puts "FQDN: #{data['fqdn']}".cyan if data['fqdn']
|
404
|
+
if data['dns_servers'] && !data['dns_servers'].empty?
|
405
|
+
puts "DNS Servers: #{data['dns_servers'].join(', ')}".cyan
|
406
|
+
end
|
407
|
+
end
|
408
|
+
when 'set', 'configure'
|
409
|
+
if !options[:ip] && !options[:hostname] && !options[:dns1]
|
410
|
+
error "Must provide at least --ip, --hostname, or --dns1"
|
411
|
+
return
|
412
|
+
end
|
413
|
+
|
414
|
+
if options[:ip] && !options[:mask]
|
415
|
+
error "Must provide --mask when setting --ip"
|
416
|
+
return
|
417
|
+
end
|
418
|
+
|
419
|
+
result = client.set_bmc_network(
|
420
|
+
ip_address: options[:ip],
|
421
|
+
subnet_mask: options[:mask],
|
422
|
+
gateway: options[:gateway],
|
423
|
+
dns_primary: options[:dns1],
|
424
|
+
dns_secondary: options[:dns2],
|
425
|
+
hostname: options[:hostname]
|
426
|
+
)
|
427
|
+
output_result({ success: result }, result ? "Network configured successfully" : "Failed to configure network")
|
428
|
+
when 'dhcp'
|
429
|
+
result = client.set_bmc_dhcp
|
430
|
+
output_result({ success: result }, result ? "BMC set to DHCP mode" : "Failed to set DHCP mode")
|
431
|
+
else
|
432
|
+
error "Unknown network command: #{subcommand}"
|
433
|
+
puts "Available: show, set, dhcp"
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
380
438
|
# Config Commands
|
381
439
|
desc "config SUBCOMMAND", "Configuration file management"
|
382
440
|
def config(subcommand = 'generate')
|
data/lib/radfish/client.rb
CHANGED
@@ -24,7 +24,7 @@ module Radfish
|
|
24
24
|
@vendor = detector.detect
|
25
25
|
|
26
26
|
if @vendor.nil?
|
27
|
-
raise UnsupportedVendorError, "Could not detect vendor for #{host}"
|
27
|
+
raise UnsupportedVendorError, "Could not detect vendor for #{host}:#{options[:port] || 443}. Please check: 1) The host is reachable, 2) Credentials are correct (#{username}), 3) The BMC supports Redfish API"
|
28
28
|
end
|
29
29
|
|
30
30
|
debug "Auto-detected vendor: #{@vendor}", 1, :green
|
@@ -113,6 +113,33 @@ module Radfish
|
|
113
113
|
@adapter.class
|
114
114
|
end
|
115
115
|
|
116
|
+
# Lazy-loading API methods that return structured data
|
117
|
+
|
118
|
+
def system
|
119
|
+
@system ||= SystemInfo.new(self)
|
120
|
+
end
|
121
|
+
|
122
|
+
def bmc
|
123
|
+
@bmc ||= BmcInfo.new(self)
|
124
|
+
end
|
125
|
+
|
126
|
+
def power
|
127
|
+
@power ||= PowerInfo.new(self)
|
128
|
+
end
|
129
|
+
|
130
|
+
def thermal
|
131
|
+
@thermal ||= ThermalInfo.new(self)
|
132
|
+
end
|
133
|
+
|
134
|
+
def pci
|
135
|
+
@pci ||= PciInfo.new(self)
|
136
|
+
end
|
137
|
+
|
138
|
+
def service_tag
|
139
|
+
# Get service_tag from system info
|
140
|
+
system.service_tag
|
141
|
+
end
|
142
|
+
|
116
143
|
def supported_features
|
117
144
|
# Return a list of features this adapter supports
|
118
145
|
features = []
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'logger'
|
4
|
+
|
3
5
|
module Radfish
|
4
6
|
module Core
|
5
7
|
class BaseClient
|
@@ -23,26 +25,49 @@ module Radfish
|
|
23
25
|
|
24
26
|
# Store any vendor-specific options
|
25
27
|
@options = options
|
28
|
+
|
29
|
+
# Create the HTTP client
|
30
|
+
@http_client = HttpClient.new(
|
31
|
+
host: host,
|
32
|
+
port: port,
|
33
|
+
use_ssl: use_ssl,
|
34
|
+
verify_ssl: verify_ssl,
|
35
|
+
username: username,
|
36
|
+
password: password,
|
37
|
+
verbosity: verbosity,
|
38
|
+
retry_count: retry_count,
|
39
|
+
retry_delay: retry_delay
|
40
|
+
)
|
26
41
|
end
|
27
42
|
|
28
43
|
def base_url
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
@http_client.base_url
|
45
|
+
end
|
46
|
+
|
47
|
+
def verbosity=(value)
|
48
|
+
@verbosity = value
|
49
|
+
@http_client.verbosity = value if @http_client
|
50
|
+
end
|
51
|
+
|
52
|
+
# Delegate HTTP methods to the client
|
53
|
+
def http_get(path, **options)
|
54
|
+
@http_client.get(path, **options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def http_post(path, **options)
|
58
|
+
@http_client.post(path, **options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def http_put(path, **options)
|
62
|
+
@http_client.put(path, **options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def http_patch(path, **options)
|
66
|
+
@http_client.patch(path, **options)
|
67
|
+
end
|
68
|
+
|
69
|
+
def http_delete(path, **options)
|
70
|
+
@http_client.delete(path, **options)
|
46
71
|
end
|
47
72
|
|
48
73
|
def with_retries(max_retries = nil, initial_delay = nil, error_classes = nil)
|
@@ -104,6 +129,13 @@ module Radfish
|
|
104
129
|
end
|
105
130
|
end
|
106
131
|
|
132
|
+
protected
|
133
|
+
|
134
|
+
# Access to the underlying HTTP client for subclasses
|
135
|
+
def http_client
|
136
|
+
@http_client
|
137
|
+
end
|
138
|
+
|
107
139
|
# Helper for handling responses
|
108
140
|
def handle_response(response)
|
109
141
|
if response.headers["location"]
|
data/lib/radfish/core/boot.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
module Radfish
|
4
4
|
module Core
|
5
5
|
module Boot
|
6
|
-
def
|
7
|
-
raise NotImplementedError, "Adapter must implement #
|
6
|
+
def boot_config
|
7
|
+
raise NotImplementedError, "Adapter must implement #boot_config"
|
8
8
|
end
|
9
9
|
|
10
10
|
def set_boot_override(target, persistent: false)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radfish
|
4
|
+
module Core
|
5
|
+
module Network
|
6
|
+
def get_bmc_network
|
7
|
+
raise NotImplementedError, "Adapter must implement #get_bmc_network"
|
8
|
+
end
|
9
|
+
|
10
|
+
def set_bmc_network(ip_address: nil, subnet_mask: nil, gateway: nil,
|
11
|
+
dns_primary: nil, dns_secondary: nil, hostname: nil,
|
12
|
+
dhcp: false)
|
13
|
+
raise NotImplementedError, "Adapter must implement #set_bmc_network"
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_bmc_dhcp
|
17
|
+
# Convenience method that calls set_bmc_network with dhcp: true
|
18
|
+
set_bmc_network(dhcp: true)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday/multipart'
|
5
|
+
require 'faraday/retry'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module Radfish
|
9
|
+
# Shared HTTP client for all BMC connections
|
10
|
+
class HttpClient
|
11
|
+
include Debuggable
|
12
|
+
|
13
|
+
attr_reader :host, :port, :use_ssl, :verify_ssl
|
14
|
+
attr_accessor :username, :password, :verbosity, :retry_count, :retry_delay
|
15
|
+
|
16
|
+
def initialize(host:, port: 443, use_ssl: true, verify_ssl: false,
|
17
|
+
username: nil, password: nil, verbosity: 0,
|
18
|
+
retry_count: 3, retry_delay: 1, **options)
|
19
|
+
@host = host
|
20
|
+
@port = port
|
21
|
+
@use_ssl = use_ssl
|
22
|
+
@verify_ssl = verify_ssl
|
23
|
+
@username = username
|
24
|
+
@password = password
|
25
|
+
@verbosity = verbosity
|
26
|
+
@retry_count = retry_count
|
27
|
+
@retry_delay = retry_delay
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def base_url
|
32
|
+
protocol = use_ssl ? 'https' : 'http'
|
33
|
+
"#{protocol}://#{host}:#{port}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(path, headers: {}, **options)
|
37
|
+
request(:get, path, headers: headers, **options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def post(path, body: nil, headers: {}, **options)
|
41
|
+
request(:post, path, body: body, headers: headers, **options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def put(path, body: nil, headers: {}, **options)
|
45
|
+
request(:put, path, body: body, headers: headers, **options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def patch(path, body: nil, headers: {}, **options)
|
49
|
+
request(:patch, path, body: body, headers: headers, **options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(path, headers: {}, **options)
|
53
|
+
request(:delete, path, headers: headers, **options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def request(method, path, body: nil, headers: {}, auth: true, timeout: nil, **options)
|
57
|
+
response = connection(auth: auth).send(method) do |req|
|
58
|
+
req.url path
|
59
|
+
req.headers.merge!(headers)
|
60
|
+
req.body = body if body
|
61
|
+
|
62
|
+
# Override timeout if specified
|
63
|
+
if timeout
|
64
|
+
req.options.timeout = timeout
|
65
|
+
req.options.open_timeout = [timeout / 2, 5].min
|
66
|
+
end
|
67
|
+
|
68
|
+
# Apply any additional options
|
69
|
+
options.each do |key, value|
|
70
|
+
req.options[key] = value if req.options.respond_to?(:"#{key}=")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
response
|
75
|
+
rescue Faraday::ConnectionFailed => e
|
76
|
+
debug "Connection failed: #{e.message}", 1, :red
|
77
|
+
raise ConnectionError, "Failed to connect to #{host}: #{e.message}"
|
78
|
+
rescue Faraday::TimeoutError => e
|
79
|
+
debug "Request timed out: #{e.message}", 1, :red
|
80
|
+
raise TimeoutError, "Request to #{host} timed out: #{e.message}"
|
81
|
+
rescue Faraday::SSLError => e
|
82
|
+
debug "SSL error: #{e.message}", 1, :red
|
83
|
+
raise ConnectionError, "SSL error connecting to #{host}: #{e.message}"
|
84
|
+
rescue => e
|
85
|
+
debug "HTTP request failed: #{e.class} - #{e.message}", 1, :red
|
86
|
+
raise Error, "HTTP request failed: #{e.message}"
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def connection(auth: true)
|
92
|
+
@connections ||= {}
|
93
|
+
cache_key = auth ? :with_auth : :without_auth
|
94
|
+
|
95
|
+
@connections[cache_key] ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday|
|
96
|
+
# Add authentication if credentials provided and auth is enabled
|
97
|
+
if auth && username && password
|
98
|
+
faraday.request :authorization, :basic, username, password
|
99
|
+
end
|
100
|
+
|
101
|
+
# Standard headers
|
102
|
+
faraday.headers['Accept'] = 'application/json'
|
103
|
+
faraday.headers['Content-Type'] = 'application/json'
|
104
|
+
faraday.headers['Connection'] = 'keep-alive'
|
105
|
+
|
106
|
+
# Enable multipart for file uploads
|
107
|
+
faraday.request :multipart
|
108
|
+
faraday.request :url_encoded
|
109
|
+
|
110
|
+
# Add retry middleware for robustness
|
111
|
+
faraday.request :retry, {
|
112
|
+
max: retry_count,
|
113
|
+
interval: retry_delay,
|
114
|
+
interval_randomness: 0.5,
|
115
|
+
backoff_factor: 2,
|
116
|
+
exceptions: [
|
117
|
+
Faraday::ConnectionFailed,
|
118
|
+
Faraday::TimeoutError,
|
119
|
+
Faraday::RetriableResponse
|
120
|
+
],
|
121
|
+
methods: [:get, :put, :delete, :post, :patch],
|
122
|
+
retry_statuses: [408, 429, 500, 502, 503, 504],
|
123
|
+
retry_block: -> (env, options, retries, exception) {
|
124
|
+
if verbosity > 0
|
125
|
+
debug "Retry #{retries}/#{options[:max]}: #{exception&.message || "HTTP #{env.status}"}", 1, :yellow
|
126
|
+
end
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
# Set timeouts
|
131
|
+
faraday.options.timeout = 30
|
132
|
+
faraday.options.open_timeout = 10
|
133
|
+
|
134
|
+
# Add logging if verbose
|
135
|
+
if verbosity > 0
|
136
|
+
faraday.response :logger, Logger.new(STDOUT), bodies: verbosity >= 2 do |logger|
|
137
|
+
logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
|
138
|
+
logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
|
139
|
+
logger.filter(/("password":\s*")([^"]+)/, '\1[FILTERED]')
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
faraday.adapter Faraday.default_adapter
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radfish
|
4
|
+
class PciInfo
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
@cache = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get all PCI devices
|
13
|
+
def devices
|
14
|
+
@cache[:devices] ||= @client.adapter.pci_devices
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get NICs with PCI slot information
|
18
|
+
def nics_with_slots
|
19
|
+
@cache[:nics_with_slots] ||= @client.adapter.nics_with_pci_info
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find PCI devices by manufacturer
|
23
|
+
def devices_by_manufacturer(manufacturer)
|
24
|
+
devices.select { |d| d.manufacturer&.match?(/#{manufacturer}/i) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Find Mellanox devices
|
28
|
+
def mellanox_devices
|
29
|
+
devices_by_manufacturer('Mellanox')
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get network controllers
|
33
|
+
def network_controllers
|
34
|
+
devices.select { |d| d.device_class&.match?(/NetworkController/i) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radfish
|
4
|
+
class PowerInfo
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
@cache = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def keys
|
13
|
+
[:state, :usage_watts, :capacity_watts, :allocated_watts, :reset_types_allowed, :psus, :on, :off, :restart, :cycle]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
# Only include data attributes, not methods
|
18
|
+
data_keys = [:state, :usage_watts, :capacity_watts, :allocated_watts, :reset_types_allowed, :psus]
|
19
|
+
data_keys.each_with_object({}) do |key, hash|
|
20
|
+
hash[key] = send(key) rescue nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def state
|
25
|
+
fetch_power_status[:power_state] || fetch_power_status[:state]
|
26
|
+
end
|
27
|
+
|
28
|
+
def usage_watts
|
29
|
+
fetch_power_consumption[:consumed_watts] ||
|
30
|
+
fetch_power_consumption[:power_usage_watts] ||
|
31
|
+
fetch_power_consumption_watts
|
32
|
+
end
|
33
|
+
|
34
|
+
def capacity_watts
|
35
|
+
fetch_power_consumption[:capacity_watts] || fetch_power_consumption[:power_capacity_watts]
|
36
|
+
end
|
37
|
+
|
38
|
+
def allocated_watts
|
39
|
+
fetch_power_consumption[:allocated_watts] || fetch_power_consumption[:power_allocated_watts]
|
40
|
+
end
|
41
|
+
|
42
|
+
def on
|
43
|
+
@client.adapter.power_on
|
44
|
+
end
|
45
|
+
|
46
|
+
def off(force: false)
|
47
|
+
@client.adapter.power_off(force: force)
|
48
|
+
end
|
49
|
+
|
50
|
+
def restart(force: false)
|
51
|
+
@client.adapter.power_restart(force: force)
|
52
|
+
end
|
53
|
+
|
54
|
+
def cycle
|
55
|
+
@client.adapter.power_cycle
|
56
|
+
end
|
57
|
+
|
58
|
+
def reset_types_allowed
|
59
|
+
@cache[:reset_types] ||= @client.adapter.reset_type_allowed if @client.adapter.respond_to?(:reset_type_allowed)
|
60
|
+
end
|
61
|
+
|
62
|
+
def psus
|
63
|
+
@cache[:psus] ||= @client.adapter.psus
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def fetch_power_status
|
69
|
+
@cache[:power_status] ||= begin
|
70
|
+
if @client.adapter.respond_to?(:power_status)
|
71
|
+
status = @client.adapter.power_status
|
72
|
+
case status
|
73
|
+
when Hash
|
74
|
+
status
|
75
|
+
when String, Symbol
|
76
|
+
{ power_state: status.to_s }
|
77
|
+
else
|
78
|
+
{ power_state: status }
|
79
|
+
end
|
80
|
+
else
|
81
|
+
{}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def fetch_power_consumption
|
87
|
+
@cache[:power_consumption] ||= begin
|
88
|
+
if @client.adapter.respond_to?(:power_consumption)
|
89
|
+
@client.adapter.power_consumption
|
90
|
+
else
|
91
|
+
{}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def fetch_power_consumption_watts
|
97
|
+
@cache[:power_consumption_watts] ||= begin
|
98
|
+
if @client.adapter.respond_to?(:power_consumption_watts)
|
99
|
+
@client.adapter.power_consumption_watts
|
100
|
+
else
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radfish
|
4
|
+
class SystemInfo
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
@cache = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def keys
|
13
|
+
[:service_tag, :make, :model, :serial, :cpus, :memory, :nics, :fans, :psus, :health, :controllers]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
keys.each_with_object({}) do |key, hash|
|
18
|
+
hash[key] = send(key)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def service_tag
|
23
|
+
fetch_system_info[:service_tag]
|
24
|
+
end
|
25
|
+
|
26
|
+
def make
|
27
|
+
fetch_system_info[:manufacturer] || fetch_system_info[:make]
|
28
|
+
end
|
29
|
+
|
30
|
+
def model
|
31
|
+
fetch_system_info[:model]
|
32
|
+
end
|
33
|
+
|
34
|
+
def serial
|
35
|
+
fetch_system_info[:serial_number] || fetch_system_info[:serial]
|
36
|
+
end
|
37
|
+
|
38
|
+
def cpus
|
39
|
+
@cache[:cpus] ||= @client.adapter.cpus
|
40
|
+
end
|
41
|
+
|
42
|
+
def memory
|
43
|
+
@cache[:memory] ||= @client.adapter.memory
|
44
|
+
end
|
45
|
+
|
46
|
+
def nics
|
47
|
+
@cache[:nics] ||= @client.adapter.nics
|
48
|
+
end
|
49
|
+
|
50
|
+
def fans
|
51
|
+
@cache[:fans] ||= @client.adapter.fans
|
52
|
+
end
|
53
|
+
|
54
|
+
# Removed temperatures - not universally supported
|
55
|
+
|
56
|
+
def psus
|
57
|
+
@cache[:psus] ||= @client.adapter.psus
|
58
|
+
end
|
59
|
+
|
60
|
+
def health
|
61
|
+
@cache[:health] ||= @client.adapter.system_health
|
62
|
+
end
|
63
|
+
|
64
|
+
def controllers
|
65
|
+
@cache[:controllers] ||= @client.adapter.storage_controllers
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def fetch_system_info
|
71
|
+
@cache[:system_info] ||= @client.adapter.system_info
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radfish
|
4
|
+
class ThermalInfo
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
@cache = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def keys
|
13
|
+
[:fans]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_h
|
17
|
+
keys.each_with_object({}) do |key, hash|
|
18
|
+
hash[key] = send(key) rescue nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def fans
|
23
|
+
@cache[:fans] ||= @client.adapter.fans
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,12 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'net/http'
|
4
3
|
require 'json'
|
5
|
-
require 'uri'
|
6
|
-
require 'openssl'
|
7
4
|
|
8
5
|
module Radfish
|
9
6
|
class VendorDetector
|
7
|
+
include Debuggable
|
10
8
|
|
11
9
|
attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl
|
12
10
|
attr_accessor :verbosity
|
@@ -19,17 +17,39 @@ module Radfish
|
|
19
17
|
@use_ssl = use_ssl
|
20
18
|
@verify_ssl = verify_ssl
|
21
19
|
@verbosity = 0
|
20
|
+
|
21
|
+
# Use the shared HTTP client
|
22
|
+
@http_client = HttpClient.new(
|
23
|
+
host: host,
|
24
|
+
port: port,
|
25
|
+
use_ssl: use_ssl,
|
26
|
+
verify_ssl: verify_ssl,
|
27
|
+
username: username,
|
28
|
+
password: password,
|
29
|
+
verbosity: 0, # Will be updated via verbosity= setter
|
30
|
+
retry_count: 2, # Fewer retries for detection
|
31
|
+
retry_delay: 0.5
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def verbosity=(value)
|
36
|
+
@verbosity = value
|
37
|
+
@http_client.verbosity = value if @http_client
|
22
38
|
end
|
23
39
|
|
24
40
|
def detect
|
25
|
-
|
41
|
+
debug "Detecting vendor for #{host}:#{port}...", 1, :cyan
|
26
42
|
|
27
43
|
# Try to get the Redfish service root
|
28
44
|
service_root = fetch_service_root
|
29
|
-
|
45
|
+
|
46
|
+
unless service_root
|
47
|
+
debug "Failed to fetch service root from #{host}:#{port}", 1, :red
|
48
|
+
return nil
|
49
|
+
end
|
30
50
|
|
31
51
|
vendor = identify_vendor(service_root)
|
32
|
-
|
52
|
+
debug "Detected vendor: #{vendor || 'Unknown'} for #{host}:#{port}", 1, vendor ? :green : :yellow
|
33
53
|
|
34
54
|
vendor
|
35
55
|
end
|
@@ -37,29 +57,38 @@ module Radfish
|
|
37
57
|
private
|
38
58
|
|
39
59
|
def fetch_service_root
|
40
|
-
uri = URI("#{base_url}/redfish/v1")
|
41
|
-
|
42
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
43
|
-
http.use_ssl = use_ssl
|
44
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless verify_ssl
|
45
|
-
http.open_timeout = 5
|
46
|
-
http.read_timeout = 10
|
47
|
-
|
48
|
-
req = Net::HTTP::Get.new(uri)
|
49
|
-
req.basic_auth(username, password)
|
50
|
-
req['Accept'] = 'application/json'
|
51
|
-
|
52
60
|
begin
|
53
|
-
|
61
|
+
# Use a shorter timeout for vendor detection (5 seconds total)
|
62
|
+
response = @http_client.get('/redfish/v1', timeout: 5)
|
54
63
|
|
55
|
-
if
|
56
|
-
JSON.parse(
|
64
|
+
if response.status == 200
|
65
|
+
JSON.parse(response.body)
|
66
|
+
elsif response.status == 401
|
67
|
+
debug "Authentication failed (HTTP 401) - check username/password", 1, :red
|
68
|
+
nil
|
69
|
+
elsif response.status == 404
|
70
|
+
debug "Redfish API not found at /redfish/v1 (HTTP 404)", 1, :red
|
71
|
+
nil
|
57
72
|
else
|
58
|
-
|
73
|
+
debug "Failed to fetch service root: HTTP #{response.status}", 1, :red
|
74
|
+
debug "Response body: #{response.body[0..200]}" if response.body && @verbosity >= 2
|
59
75
|
nil
|
60
76
|
end
|
77
|
+
rescue ConnectionError, TimeoutError => e
|
78
|
+
debug "Connection failed to #{host}:#{port} - #{e.message}", 1, :red
|
79
|
+
nil
|
80
|
+
rescue JSON::ParserError => e
|
81
|
+
debug "Invalid JSON response from BMC: #{e.message}", 1, :red
|
82
|
+
nil
|
83
|
+
rescue Faraday::ConnectionFailed => e
|
84
|
+
debug "Connection refused or failed to #{host}:#{port} - #{e.message}", 1, :red
|
85
|
+
nil
|
86
|
+
rescue Faraday::TimeoutError => e
|
87
|
+
debug "Request timed out to #{host}:#{port} - #{e.message}", 1, :red
|
88
|
+
nil
|
61
89
|
rescue => e
|
62
|
-
|
90
|
+
debug "Unexpected error fetching service root: #{e.class} - #{e.message}", 1, :red
|
91
|
+
debug "Backtrace: #{e.backtrace.first(3).join("\n")}" if @verbosity >= 2
|
63
92
|
nil
|
64
93
|
end
|
65
94
|
end
|
@@ -101,23 +130,11 @@ module Radfish
|
|
101
130
|
end
|
102
131
|
|
103
132
|
def detect_from_managers(managers_path)
|
104
|
-
uri = URI("#{base_url}#{managers_path}")
|
105
|
-
|
106
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
107
|
-
http.use_ssl = use_ssl
|
108
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless verify_ssl
|
109
|
-
http.open_timeout = 5
|
110
|
-
http.read_timeout = 10
|
111
|
-
|
112
|
-
req = Net::HTTP::Get.new(uri)
|
113
|
-
req.basic_auth(username, password)
|
114
|
-
req['Accept'] = 'application/json'
|
115
|
-
|
116
133
|
begin
|
117
|
-
|
134
|
+
response = @http_client.get(managers_path)
|
118
135
|
|
119
|
-
if
|
120
|
-
data = JSON.parse(
|
136
|
+
if response.status == 200
|
137
|
+
data = JSON.parse(response.body)
|
121
138
|
|
122
139
|
# Check first manager
|
123
140
|
if data['Members'] && data['Members'].first
|
@@ -135,7 +152,6 @@ module Radfish
|
|
135
152
|
# Check manager model/description
|
136
153
|
model = manager_data['Model'] || ''
|
137
154
|
description = manager_data['Description'] || ''
|
138
|
-
# firmware = manager_data['FirmwareVersion'] || '' # Reserved for future use
|
139
155
|
|
140
156
|
return 'dell' if model.match?(/idrac/i) || description.match?(/idrac/i)
|
141
157
|
return 'hpe' if model.match?(/ilo/i) || description.match?(/ilo/i)
|
@@ -146,30 +162,18 @@ module Radfish
|
|
146
162
|
end
|
147
163
|
end
|
148
164
|
rescue => e
|
149
|
-
|
165
|
+
debug "Error detecting from managers: #{e.message}", 3, :yellow
|
150
166
|
end
|
151
167
|
|
152
168
|
nil
|
153
169
|
end
|
154
170
|
|
155
171
|
def fetch_manager(manager_path)
|
156
|
-
uri = URI("#{base_url}#{manager_path}")
|
157
|
-
|
158
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
159
|
-
http.use_ssl = use_ssl
|
160
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless verify_ssl
|
161
|
-
http.open_timeout = 5
|
162
|
-
http.read_timeout = 10
|
163
|
-
|
164
|
-
req = Net::HTTP::Get.new(uri)
|
165
|
-
req.basic_auth(username, password)
|
166
|
-
req['Accept'] = 'application/json'
|
167
|
-
|
168
172
|
begin
|
169
|
-
|
170
|
-
JSON.parse(
|
173
|
+
response = @http_client.get(manager_path)
|
174
|
+
JSON.parse(response.body) if response.status == 200
|
171
175
|
rescue => e
|
172
|
-
|
176
|
+
debug "Error fetching manager: #{e.message}", 3, :yellow
|
173
177
|
nil
|
174
178
|
end
|
175
179
|
end
|
@@ -190,10 +194,5 @@ module Radfish
|
|
190
194
|
vendor_string.to_s.downcase
|
191
195
|
end
|
192
196
|
end
|
193
|
-
|
194
|
-
def base_url
|
195
|
-
protocol = use_ssl ? 'https' : 'http'
|
196
|
-
"#{protocol}://#{host}:#{port}"
|
197
|
-
end
|
198
197
|
end
|
199
198
|
end
|
data/lib/radfish/version.rb
CHANGED
data/lib/radfish.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'httparty'
|
4
3
|
require 'faraday'
|
5
4
|
require 'faraday/multipart'
|
5
|
+
require 'faraday/retry'
|
6
6
|
require 'base64'
|
7
7
|
require 'uri'
|
8
8
|
require 'json'
|
@@ -17,6 +17,18 @@ module Radfish
|
|
17
17
|
class NotFoundError < Error; end
|
18
18
|
class TimeoutError < Error; end
|
19
19
|
class UnsupportedVendorError < Error; end
|
20
|
+
|
21
|
+
# Virtual Media specific errors
|
22
|
+
class VirtualMediaError < Error; end
|
23
|
+
class VirtualMediaNotFoundError < VirtualMediaError; end
|
24
|
+
class VirtualMediaConnectionError < VirtualMediaError; end
|
25
|
+
class VirtualMediaLicenseError < VirtualMediaError; end
|
26
|
+
class VirtualMediaBusyError < VirtualMediaError; end
|
27
|
+
|
28
|
+
# Task/Job errors
|
29
|
+
class TaskError < Error; end
|
30
|
+
class TaskTimeoutError < TaskError; end
|
31
|
+
class TaskFailedError < TaskError; end
|
20
32
|
|
21
33
|
module Debuggable
|
22
34
|
def debug(message, level = 1, color = :light_cyan)
|
@@ -62,18 +74,25 @@ module Radfish
|
|
62
74
|
end
|
63
75
|
end
|
64
76
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
+
require_relative 'radfish/version'
|
78
|
+
require_relative 'radfish/http_client'
|
79
|
+
require_relative 'radfish/core/base_client'
|
80
|
+
require_relative 'radfish/core/session'
|
81
|
+
require_relative 'radfish/core/power'
|
82
|
+
require_relative 'radfish/core/system'
|
83
|
+
require_relative 'radfish/core/storage'
|
84
|
+
require_relative 'radfish/core/virtual_media'
|
85
|
+
require_relative 'radfish/core/boot'
|
86
|
+
require_relative 'radfish/core/jobs'
|
87
|
+
require_relative 'radfish/core/utility'
|
88
|
+
require_relative 'radfish/core/network'
|
89
|
+
require_relative 'radfish/vendor_detector'
|
90
|
+
require_relative 'radfish/system_info'
|
91
|
+
require_relative 'radfish/bmc_info'
|
92
|
+
require_relative 'radfish/power_info'
|
93
|
+
require_relative 'radfish/thermal_info'
|
94
|
+
require_relative 'radfish/pci_info'
|
95
|
+
require_relative 'radfish/client'
|
77
96
|
|
78
97
|
# Auto-load adapters if available
|
79
98
|
begin
|
data/radfish.gemspec
CHANGED
@@ -29,9 +29,9 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.require_paths = ["lib"]
|
30
30
|
|
31
31
|
spec.add_dependency "thor", "~> 1.2"
|
32
|
-
spec.add_dependency "httparty", "~> 0.21"
|
33
32
|
spec.add_dependency "faraday", "~> 2.0"
|
34
33
|
spec.add_dependency "faraday-multipart", "~> 1.0"
|
34
|
+
spec.add_dependency "faraday-retry", "~> 2.0"
|
35
35
|
spec.add_dependency "nokogiri", "~> 1.14"
|
36
36
|
spec.add_dependency "colorize", ">= 0.8"
|
37
37
|
spec.add_dependency "activesupport", ">= 7.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: radfish
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -25,47 +25,47 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: faraday
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0
|
33
|
+
version: '2.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0
|
40
|
+
version: '2.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: faraday
|
42
|
+
name: faraday-multipart
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '1.0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '1.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name: faraday-
|
56
|
+
name: faraday-retry
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '2.0'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '2.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: nokogiri
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -165,18 +165,25 @@ files:
|
|
165
165
|
- Rakefile
|
166
166
|
- exe/radfish
|
167
167
|
- lib/radfish.rb
|
168
|
+
- lib/radfish/bmc_info.rb
|
168
169
|
- lib/radfish/cli.rb
|
169
170
|
- lib/radfish/cli/base.rb
|
170
171
|
- lib/radfish/client.rb
|
171
172
|
- lib/radfish/core/base_client.rb
|
172
173
|
- lib/radfish/core/boot.rb
|
173
174
|
- lib/radfish/core/jobs.rb
|
175
|
+
- lib/radfish/core/network.rb
|
174
176
|
- lib/radfish/core/power.rb
|
175
177
|
- lib/radfish/core/session.rb
|
176
178
|
- lib/radfish/core/storage.rb
|
177
179
|
- lib/radfish/core/system.rb
|
178
180
|
- lib/radfish/core/utility.rb
|
179
181
|
- lib/radfish/core/virtual_media.rb
|
182
|
+
- lib/radfish/http_client.rb
|
183
|
+
- lib/radfish/pci_info.rb
|
184
|
+
- lib/radfish/power_info.rb
|
185
|
+
- lib/radfish/system_info.rb
|
186
|
+
- lib/radfish/thermal_info.rb
|
180
187
|
- lib/radfish/vendor_detector.rb
|
181
188
|
- lib/radfish/version.rb
|
182
189
|
- radfish.gemspec
|