radfish 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.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ class Client
5
+ include Debuggable
6
+
7
+ attr_reader :adapter, :vendor
8
+ attr_accessor :verbosity
9
+
10
+ def initialize(host:, username:, password:, vendor: nil, **options)
11
+ @verbosity = options[:verbosity] || 0
12
+
13
+ # Auto-detect vendor if not specified
14
+ if vendor.nil?
15
+ detector = VendorDetector.new(
16
+ host: host,
17
+ username: username,
18
+ password: password,
19
+ port: options[:port] || 443,
20
+ use_ssl: options.fetch(:use_ssl, true),
21
+ verify_ssl: options.fetch(:verify_ssl, false)
22
+ )
23
+ detector.verbosity = @verbosity
24
+ @vendor = detector.detect
25
+
26
+ if @vendor.nil?
27
+ raise UnsupportedVendorError, "Could not detect vendor for #{host}"
28
+ end
29
+
30
+ debug "Auto-detected vendor: #{@vendor}", 1, :green
31
+ else
32
+ @vendor = vendor.to_s.downcase
33
+ debug "Using specified vendor: #{@vendor}", 1, :cyan
34
+ end
35
+
36
+ # Get the adapter class for this vendor
37
+ adapter_class = Radfish.get_adapter(@vendor)
38
+
39
+ if adapter_class.nil?
40
+ # Try to load the adapter gem dynamically
41
+ begin
42
+ require "radfish/#{@vendor}_adapter"
43
+ adapter_class = Radfish.get_adapter(@vendor)
44
+ rescue LoadError
45
+ # Adapter gem not installed
46
+ end
47
+ end
48
+
49
+ if adapter_class.nil?
50
+ raise UnsupportedVendorError, "No adapter available for vendor: #{@vendor}. " \
51
+ "Please install the radfish-#{@vendor} gem or use a supported vendor."
52
+ end
53
+
54
+ # Create the adapter instance
55
+ @adapter = adapter_class.new(
56
+ host: host,
57
+ username: username,
58
+ password: password,
59
+ **options
60
+ )
61
+
62
+ # Pass verbosity to adapter
63
+ @adapter.verbosity = @verbosity if @adapter.respond_to?(:verbosity=)
64
+ end
65
+
66
+ def self.connect(host:, username:, password:, vendor: nil, **options)
67
+ client = new(host: host, username: username, password: password, vendor: vendor, **options)
68
+
69
+ if block_given?
70
+ begin
71
+ client.login
72
+ yield client
73
+ ensure
74
+ client.logout
75
+ end
76
+ else
77
+ client
78
+ end
79
+ end
80
+
81
+ # Delegate all method calls to the adapter
82
+ def method_missing(method, *args, **kwargs, &block)
83
+ if @adapter.respond_to?(method)
84
+ if kwargs.empty?
85
+ @adapter.send(method, *args, &block)
86
+ else
87
+ @adapter.send(method, *args, **kwargs, &block)
88
+ end
89
+ else
90
+ super
91
+ end
92
+ end
93
+
94
+ def respond_to_missing?(method, include_private = false)
95
+ @adapter.respond_to?(method, include_private) || super
96
+ end
97
+
98
+ # Core methods that should always be available
99
+
100
+ def login
101
+ @adapter.login
102
+ end
103
+
104
+ def logout
105
+ @adapter.logout
106
+ end
107
+
108
+ def vendor_name
109
+ @vendor
110
+ end
111
+
112
+ def adapter_class
113
+ @adapter.class
114
+ end
115
+
116
+ def supported_features
117
+ # Return a list of features this adapter supports
118
+ features = []
119
+
120
+ # Check which modules are included
121
+ features << :power if @adapter.respond_to?(:power_status)
122
+ features << :system if @adapter.respond_to?(:system_info)
123
+ features << :storage if @adapter.respond_to?(:storage_controllers)
124
+ features << :virtual_media if @adapter.respond_to?(:virtual_media)
125
+ features << :boot if @adapter.respond_to?(:boot_options)
126
+ features << :jobs if @adapter.respond_to?(:jobs)
127
+ features << :utility if @adapter.respond_to?(:sel_log)
128
+
129
+ features
130
+ end
131
+
132
+ def info
133
+ {
134
+ vendor: @vendor,
135
+ adapter: adapter_class.name,
136
+ features: supported_features,
137
+ host: @adapter.host,
138
+ base_url: @adapter.base_url
139
+ }
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ class BaseClient
6
+ include Debuggable
7
+
8
+ attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :host_header
9
+ attr_accessor :verbosity, :retry_count, :retry_delay
10
+
11
+ def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false,
12
+ retry_count: 3, retry_delay: 1, host_header: nil, **options)
13
+ @host = host
14
+ @username = username
15
+ @password = password
16
+ @port = port
17
+ @use_ssl = use_ssl
18
+ @verify_ssl = verify_ssl
19
+ @host_header = host_header
20
+ @verbosity = 0
21
+ @retry_count = retry_count
22
+ @retry_delay = retry_delay
23
+
24
+ # Store any vendor-specific options
25
+ @options = options
26
+ end
27
+
28
+ 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
46
+ end
47
+
48
+ def with_retries(max_retries = nil, initial_delay = nil, error_classes = nil)
49
+ max_retries ||= @retry_count
50
+ initial_delay ||= @retry_delay
51
+ error_classes ||= [StandardError]
52
+
53
+ retries = 0
54
+ begin
55
+ yield
56
+ rescue *error_classes => e
57
+ retries += 1
58
+ if retries <= max_retries
59
+ delay = initial_delay * (retries ** 1.5).to_i
60
+ debug "RETRY: #{e.message} - Attempt #{retries}/#{max_retries}, waiting #{delay}s", 1, :yellow
61
+ sleep delay
62
+ retry
63
+ else
64
+ debug "MAX RETRIES REACHED: #{e.message} after #{max_retries} attempts", 1, :red
65
+ raise e
66
+ end
67
+ end
68
+ end
69
+
70
+ # Vendor-specific methods to be overridden
71
+
72
+ def vendor
73
+ raise NotImplementedError, "Subclass must implement #vendor"
74
+ end
75
+
76
+ def login
77
+ raise NotImplementedError, "Subclass must implement #login"
78
+ end
79
+
80
+ def logout
81
+ raise NotImplementedError, "Subclass must implement #logout"
82
+ end
83
+
84
+ def authenticated_request(method, path, **options)
85
+ raise NotImplementedError, "Subclass must implement #authenticated_request"
86
+ end
87
+
88
+ def redfish_version
89
+ response = authenticated_request(:get, "/redfish/v1")
90
+ if response.status == 200
91
+ data = JSON.parse(response.body)
92
+ data["RedfishVersion"]
93
+ else
94
+ raise Error, "Failed to get Redfish version: #{response.status}"
95
+ end
96
+ end
97
+
98
+ def service_root
99
+ response = authenticated_request(:get, "/redfish/v1")
100
+ if response.status == 200
101
+ JSON.parse(response.body)
102
+ else
103
+ raise Error, "Failed to get service root: #{response.status}"
104
+ end
105
+ end
106
+
107
+ # Helper for handling responses
108
+ def handle_response(response)
109
+ if response.headers["location"]
110
+ return handle_location(response.headers["location"])
111
+ end
112
+
113
+ if response.status.between?(200, 299)
114
+ return response.body
115
+ else
116
+ raise Error, "Request failed: #{response.status} - #{response.body}"
117
+ end
118
+ end
119
+
120
+ def handle_location(location)
121
+ # Subclasses can override for vendor-specific handling
122
+ nil
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module Boot
6
+ def boot_options
7
+ raise NotImplementedError, "Adapter must implement #boot_options"
8
+ end
9
+
10
+ def set_boot_override(target, persistent: false)
11
+ raise NotImplementedError, "Adapter must implement #set_boot_override"
12
+ end
13
+
14
+ def clear_boot_override
15
+ raise NotImplementedError, "Adapter must implement #clear_boot_override"
16
+ end
17
+
18
+ def set_boot_order(devices)
19
+ raise NotImplementedError, "Adapter must implement #set_boot_order"
20
+ end
21
+
22
+ def get_boot_devices
23
+ raise NotImplementedError, "Adapter must implement #get_boot_devices"
24
+ end
25
+
26
+ def boot_to_pxe
27
+ raise NotImplementedError, "Adapter must implement #boot_to_pxe"
28
+ end
29
+
30
+ def boot_to_disk
31
+ raise NotImplementedError, "Adapter must implement #boot_to_disk"
32
+ end
33
+
34
+ def boot_to_cd
35
+ raise NotImplementedError, "Adapter must implement #boot_to_cd"
36
+ end
37
+
38
+ def boot_to_usb
39
+ raise NotImplementedError, "Adapter must implement #boot_to_usb"
40
+ end
41
+
42
+ def boot_to_bios_setup
43
+ raise NotImplementedError, "Adapter must implement #boot_to_bios_setup"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module Jobs
6
+ def jobs
7
+ raise NotImplementedError, "Adapter must implement #jobs"
8
+ end
9
+
10
+ def job_status(job_id)
11
+ raise NotImplementedError, "Adapter must implement #job_status"
12
+ end
13
+
14
+ def wait_for_job(job_id, timeout: 600)
15
+ raise NotImplementedError, "Adapter must implement #wait_for_job"
16
+ end
17
+
18
+ def cancel_job(job_id)
19
+ raise NotImplementedError, "Adapter must implement #cancel_job"
20
+ end
21
+
22
+ def clear_completed_jobs
23
+ raise NotImplementedError, "Adapter must implement #clear_completed_jobs"
24
+ end
25
+
26
+ def jobs_summary
27
+ raise NotImplementedError, "Adapter must implement #jobs_summary"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module Power
6
+ def power_status
7
+ raise NotImplementedError, "Adapter must implement #power_status"
8
+ end
9
+
10
+ def power_on
11
+ raise NotImplementedError, "Adapter must implement #power_on"
12
+ end
13
+
14
+ def power_off
15
+ raise NotImplementedError, "Adapter must implement #power_off"
16
+ end
17
+
18
+ def power_restart
19
+ raise NotImplementedError, "Adapter must implement #power_restart"
20
+ end
21
+
22
+ def power_cycle
23
+ raise NotImplementedError, "Adapter must implement #power_cycle"
24
+ end
25
+
26
+ def reset_type_allowed
27
+ raise NotImplementedError, "Adapter must implement #reset_type_allowed"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ class Session
6
+ include Debuggable
7
+
8
+ attr_reader :client, :x_auth_token, :session_id
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @x_auth_token = nil
13
+ @session_id = nil
14
+ end
15
+
16
+ def connection
17
+ @connection ||= Faraday.new(url: client.base_url, ssl: { verify: client.verify_ssl }) do |faraday|
18
+ faraday.request :url_encoded
19
+ faraday.adapter Faraday.default_adapter
20
+ end
21
+ end
22
+
23
+ def create
24
+ debug "Creating Redfish session for #{client.host}", 1
25
+
26
+ payload = {
27
+ UserName: client.username,
28
+ Password: client.password
29
+ }.to_json
30
+
31
+ headers = {
32
+ 'Content-Type' => 'application/json',
33
+ 'Accept' => 'application/json'
34
+ }
35
+ headers['Host'] = client.host_header if client.host_header
36
+
37
+ begin
38
+ response = connection.post('/redfish/v1/SessionService/Sessions', payload, headers)
39
+
40
+ if response.status == 201
41
+ @x_auth_token = response.headers['x-auth-token']
42
+
43
+ if response.headers['location']
44
+ @session_id = response.headers['location'].split('/').last
45
+ end
46
+
47
+ begin
48
+ body = JSON.parse(response.body)
49
+ @session_id ||= body["Id"] if body.is_a?(Hash)
50
+ rescue JSON::ParserError
51
+ end
52
+
53
+ debug "Session created successfully. Token: #{@x_auth_token ? @x_auth_token[0..10] + '...' : 'nil'}", 1, :green
54
+ return true
55
+ else
56
+ debug "Failed to create session. Status: #{response.status}", 1, :red
57
+ debug "Response: #{response.body}", 2
58
+ return false
59
+ end
60
+ rescue Faraday::Error => e
61
+ debug "Connection error creating session: #{e.message}", 1, :red
62
+ return false
63
+ end
64
+ end
65
+
66
+ def delete
67
+ return unless @x_auth_token && @session_id
68
+
69
+ debug "Deleting session #{@session_id}", 1
70
+
71
+ headers = {
72
+ 'X-Auth-Token' => @x_auth_token,
73
+ 'Accept' => 'application/json'
74
+ }
75
+ headers['Host'] = client.host_header if client.host_header
76
+
77
+ begin
78
+ response = connection.delete("/redfish/v1/SessionService/Sessions/#{@session_id}", nil, headers)
79
+
80
+ if response.status == 204 || response.status == 200
81
+ debug "Session deleted successfully", 1, :green
82
+ @x_auth_token = nil
83
+ @session_id = nil
84
+ return true
85
+ else
86
+ debug "Failed to delete session. Status: #{response.status}", 1, :yellow
87
+ return false
88
+ end
89
+ rescue Faraday::Error => e
90
+ debug "Error deleting session: #{e.message}", 1, :yellow
91
+ return false
92
+ end
93
+ end
94
+
95
+ def valid?
96
+ return false unless @x_auth_token
97
+
98
+ headers = {
99
+ 'X-Auth-Token' => @x_auth_token,
100
+ 'Accept' => 'application/json'
101
+ }
102
+ headers['Host'] = client.host_header if client.host_header
103
+
104
+ begin
105
+ response = connection.get("/redfish/v1/SessionService/Sessions/#{@session_id}", nil, headers)
106
+ response.status == 200
107
+ rescue
108
+ false
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def verbosity
115
+ client.verbosity
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module Storage
6
+ def storage_controllers
7
+ raise NotImplementedError, "Adapter must implement #storage_controllers"
8
+ end
9
+
10
+ def drives
11
+ raise NotImplementedError, "Adapter must implement #drives"
12
+ end
13
+
14
+ def volumes
15
+ raise NotImplementedError, "Adapter must implement #volumes"
16
+ end
17
+
18
+ def storage_summary
19
+ raise NotImplementedError, "Adapter must implement #storage_summary"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module System
6
+ def system_info
7
+ raise NotImplementedError, "Adapter must implement #system_info"
8
+ end
9
+
10
+ def cpus
11
+ raise NotImplementedError, "Adapter must implement #cpus"
12
+ end
13
+
14
+ def memory
15
+ raise NotImplementedError, "Adapter must implement #memory"
16
+ end
17
+
18
+ def nics
19
+ raise NotImplementedError, "Adapter must implement #nics"
20
+ end
21
+
22
+ def fans
23
+ raise NotImplementedError, "Adapter must implement #fans"
24
+ end
25
+
26
+ def temperatures
27
+ raise NotImplementedError, "Adapter must implement #temperatures"
28
+ end
29
+
30
+ def psus
31
+ raise NotImplementedError, "Adapter must implement #psus"
32
+ end
33
+
34
+ def power_consumption
35
+ raise NotImplementedError, "Adapter must implement #power_consumption"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module Utility
6
+ def sel_log
7
+ raise NotImplementedError, "Adapter must implement #sel_log"
8
+ end
9
+
10
+ def clear_sel_log
11
+ raise NotImplementedError, "Adapter must implement #clear_sel_log"
12
+ end
13
+
14
+ def sel_summary(limit: 10)
15
+ raise NotImplementedError, "Adapter must implement #sel_summary"
16
+ end
17
+
18
+ def accounts
19
+ raise NotImplementedError, "Adapter must implement #accounts"
20
+ end
21
+
22
+ def create_account(username:, password:, role: "Administrator")
23
+ raise NotImplementedError, "Adapter must implement #create_account"
24
+ end
25
+
26
+ def delete_account(username)
27
+ raise NotImplementedError, "Adapter must implement #delete_account"
28
+ end
29
+
30
+ def update_account_password(username:, new_password:)
31
+ raise NotImplementedError, "Adapter must implement #update_account_password"
32
+ end
33
+
34
+ def sessions
35
+ raise NotImplementedError, "Adapter must implement #sessions"
36
+ end
37
+
38
+ def service_info
39
+ raise NotImplementedError, "Adapter must implement #service_info"
40
+ end
41
+
42
+ def get_firmware_version
43
+ raise NotImplementedError, "Adapter must implement #get_firmware_version"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Radfish
4
+ module Core
5
+ module VirtualMedia
6
+ def virtual_media
7
+ raise NotImplementedError, "Adapter must implement #virtual_media"
8
+ end
9
+
10
+ def insert_virtual_media(iso_url, device: nil)
11
+ raise NotImplementedError, "Adapter must implement #insert_virtual_media"
12
+ end
13
+
14
+ def eject_virtual_media(device: nil)
15
+ raise NotImplementedError, "Adapter must implement #eject_virtual_media"
16
+ end
17
+
18
+ def virtual_media_status
19
+ raise NotImplementedError, "Adapter must implement #virtual_media_status"
20
+ end
21
+
22
+ def mount_iso_and_boot(iso_url, device: nil)
23
+ raise NotImplementedError, "Adapter must implement #mount_iso_and_boot"
24
+ end
25
+
26
+ def unmount_all_media
27
+ raise NotImplementedError, "Adapter must implement #unmount_all_media"
28
+ end
29
+ end
30
+ end
31
+ end