radfish 0.1.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fb2656b390f3016128a43472976efddf6593d7e30f3e498de88ed63122d6f75
4
- data.tar.gz: 696317fe19cbceef762da5e5ab4dad28c0b8a1f923a5677fdfcfd551f84225d5
3
+ metadata.gz: b58ebe70659a5db047e1d6efb8c8ec93ba7a7cce19f500f46204add014095566
4
+ data.tar.gz: 754e9d84d9bc6232cadaa4daf75fa4053b3d0fa9d53918b2f1160223775c85f9
5
5
  SHA512:
6
- metadata.gz: e9ba57f9683e6177abd5bce2481e3e4338e75b04769c160c20c5596094bea4a4b8b0e59fdca9a8473f350fcc2cab2d6d50a88f456f147ebdd23ea54cbe47f1f6
7
- data.tar.gz: e51c1b2ec7fc3013fa65661868dbc9a1c655009f58f0c537966991b2575af45f72a72979cd78acf8b6d4902e5460689821bb524abbf95b97fb08c1d9e72029d1
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
@@ -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
- protocol = use_ssl ? 'https' : 'http'
30
- "#{protocol}://#{host}:#{port}"
31
- end
32
-
33
- def connection
34
- @connection ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday|
35
- faraday.request :multipart
36
- faraday.request :url_encoded
37
- faraday.adapter Faraday.default_adapter
38
-
39
- if @verbosity > 0
40
- faraday.response :logger, Logger.new(STDOUT), bodies: @verbosity >= 2 do |logger|
41
- logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
42
- logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
43
- end
44
- end
45
- end
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"]
@@ -3,8 +3,8 @@
3
3
  module Radfish
4
4
  module Core
5
5
  module Boot
6
- def boot_options
7
- raise NotImplementedError, "Adapter must implement #boot_options"
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,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
- puts "Detecting vendor for #{host}..." if @verbosity && @verbosity > 0
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
- return nil unless service_root
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
- puts "Detected vendor: #{vendor || 'Unknown'}" if @verbosity && @verbosity > 0
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
- res = http.request(req)
61
+ # Use a shorter timeout for vendor detection (5 seconds total)
62
+ response = @http_client.get('/redfish/v1', timeout: 5)
54
63
 
55
- if res.code.to_i == 200
56
- JSON.parse(res.body)
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
- puts "Failed to fetch service root: HTTP #{res.code}" if @verbosity && @verbosity > 0
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
- puts "Error fetching service root: #{e.message}" if @verbosity && @verbosity > 0
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
- res = http.request(req)
134
+ response = @http_client.get(managers_path)
118
135
 
119
- if res.code.to_i == 200
120
- data = JSON.parse(res.body)
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
- puts "Error detecting from managers: #{e.message}" if @verbosity && @verbosity > 1
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
- res = http.request(req)
170
- JSON.parse(res.body) if res.code.to_i == 200
173
+ response = @http_client.get(manager_path)
174
+ JSON.parse(response.body) if response.status == 200
171
175
  rescue => e
172
- puts "Error fetching manager: #{e.message}" if @verbosity && @verbosity > 1
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Radfish
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
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,19 +74,25 @@ module Radfish
62
74
  end
63
75
  end
64
76
 
65
- require 'radfish/version'
66
- require 'radfish/core/base_client'
67
- require 'radfish/core/session'
68
- require 'radfish/core/power'
69
- require 'radfish/core/system'
70
- require 'radfish/core/storage'
71
- require 'radfish/core/virtual_media'
72
- require 'radfish/core/boot'
73
- require 'radfish/core/jobs'
74
- require 'radfish/core/utility'
75
- require 'radfish/core/network'
76
- require 'radfish/vendor_detector'
77
- require 'radfish/client'
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'
78
96
 
79
97
  # Auto-load adapters if available
80
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
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-08-28 00:00:00.000000000 Z
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: httparty
28
+ name: faraday
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.21'
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.21'
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: '2.0'
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: '2.0'
54
+ version: '1.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: faraday-multipart
56
+ name: faraday-retry
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.0'
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: '1.0'
68
+ version: '2.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: nokogiri
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -165,6 +165,7 @@ 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
@@ -178,6 +179,11 @@ files:
178
179
  - lib/radfish/core/system.rb
179
180
  - lib/radfish/core/utility.rb
180
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
181
187
  - lib/radfish/vendor_detector.rb
182
188
  - lib/radfish/version.rb
183
189
  - radfish.gemspec