supermicro 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,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Supermicro
6
+ module License
7
+ # Check if the BMC has the required licenses for virtual media
8
+ def check_virtual_media_license
9
+ response = authenticated_request(:get, "/redfish/v1/Managers/1/LicenseManager/QueryLicense")
10
+
11
+ if response.status != 200
12
+ debug "Unable to query license status: #{response.status}", 1, :yellow
13
+ return {
14
+ available: :unknown,
15
+ message: "Unable to query license status",
16
+ licenses: []
17
+ }
18
+ end
19
+
20
+ begin
21
+ data = JSON.parse(response.body)
22
+ licenses = data["Licenses"] || []
23
+
24
+ found_licenses = []
25
+ has_oob = false
26
+ has_dcms = false
27
+
28
+ licenses.each do |license_json|
29
+ # Parse the nested JSON structure
30
+ license_data = JSON.parse(license_json) rescue nil
31
+ next unless license_data
32
+
33
+ license_name = license_data.dig("ProductKey", "Node", "LicenseName")
34
+ next unless license_name
35
+
36
+ found_licenses << license_name
37
+
38
+ # Check for required licenses
39
+ has_oob = true if license_name == "SFT-OOB-LIC"
40
+ has_dcms = true if license_name == "SFT-DCMS-SINGLE"
41
+ end
42
+
43
+ # Virtual media requires either SFT-OOB-LIC or SFT-DCMS-SINGLE
44
+ has_required_license = has_oob || has_dcms
45
+
46
+ {
47
+ available: has_required_license,
48
+ licenses: found_licenses,
49
+ message: if has_required_license
50
+ "Virtual media license present: #{found_licenses.join(', ')}"
51
+ elsif found_licenses.empty?
52
+ "No licenses found. Virtual media requires SFT-OOB-LIC or SFT-DCMS-SINGLE"
53
+ else
54
+ "Virtual media requires SFT-OOB-LIC or SFT-DCMS-SINGLE. Found: #{found_licenses.join(', ')}"
55
+ end
56
+ }
57
+ rescue JSON::ParserError => e
58
+ debug "Failed to parse license response: #{e.message}", 1, :red
59
+ {
60
+ available: :unknown,
61
+ message: "Failed to parse license response",
62
+ licenses: []
63
+ }
64
+ end
65
+ end
66
+
67
+ # Get all licenses
68
+ def licenses
69
+ response = authenticated_request(:get, "/redfish/v1/Managers/1/LicenseManager/QueryLicense")
70
+
71
+ return [] if response.status != 200
72
+
73
+ begin
74
+ data = JSON.parse(response.body)
75
+ licenses = data["Licenses"] || []
76
+
77
+ parsed_licenses = []
78
+ licenses.each do |license_json|
79
+ license_data = JSON.parse(license_json) rescue nil
80
+ next unless license_data
81
+
82
+ node = license_data.dig("ProductKey", "Node") || {}
83
+ parsed_licenses << {
84
+ id: node["LicenseID"],
85
+ name: node["LicenseName"],
86
+ created: node["CreateDate"]
87
+ }
88
+ end
89
+
90
+ parsed_licenses
91
+ rescue JSON::ParserError
92
+ []
93
+ end
94
+ end
95
+
96
+ # Activate a license
97
+ def activate_license(license_key)
98
+ response = authenticated_request(
99
+ :post,
100
+ "/redfish/v1/Managers/1/LicenseManager/Actions/LicenseManager.ActivateLicense",
101
+ body: { "LicenseKey" => license_key }.to_json,
102
+ headers: { 'Content-Type': 'application/json' }
103
+ )
104
+
105
+ if response.status.between?(200, 299)
106
+ debug "License activated successfully", 1, :green
107
+ true
108
+ else
109
+ debug "Failed to activate license: #{response.status}", 1, :red
110
+ false
111
+ end
112
+ end
113
+
114
+ # Clear/remove a license
115
+ def clear_license(license_id)
116
+ response = authenticated_request(
117
+ :post,
118
+ "/redfish/v1/Managers/1/LicenseManager/Actions/LicenseManager.ClearLicense",
119
+ body: { "LicenseID" => license_id }.to_json,
120
+ headers: { 'Content-Type': 'application/json' }
121
+ )
122
+
123
+ if response.status.between?(200, 299)
124
+ debug "License cleared successfully", 1, :green
125
+ true
126
+ else
127
+ debug "Failed to clear license: #{response.status}", 1, :red
128
+ false
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'colorize'
5
+
6
+ module Supermicro
7
+ module Power
8
+ def power_status
9
+ response = authenticated_request(:get, "/redfish/v1/Systems/1?$select=PowerState")
10
+
11
+ if response.status == 200
12
+ begin
13
+ data = JSON.parse(response.body)
14
+ power_state = data["PowerState"]
15
+ return power_state
16
+ rescue JSON::ParserError
17
+ raise Error, "Failed to parse power status response: #{response.body}"
18
+ end
19
+ else
20
+ raise Error, "Failed to get power status. Status code: #{response.status}"
21
+ end
22
+ end
23
+
24
+ def power_on
25
+ current_state = power_status
26
+
27
+ if current_state == "On"
28
+ puts "System is already powered on.".yellow
29
+ return true
30
+ end
31
+
32
+ puts "Powering on system...".yellow
33
+
34
+ response = authenticated_request(
35
+ :post,
36
+ "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
37
+ body: { "ResetType": "On" }.to_json,
38
+ headers: { 'Content-Type': 'application/json' }
39
+ )
40
+
41
+ if response.status.between?(200, 299)
42
+ puts "Power on command sent successfully.".green
43
+ return true
44
+ else
45
+ raise Error, "Failed to power on system: #{response.status} - #{response.body}"
46
+ end
47
+ end
48
+
49
+ def power_off(force: false)
50
+ current_state = power_status
51
+
52
+ if current_state == "Off"
53
+ puts "System is already powered off.".yellow
54
+ return true
55
+ end
56
+
57
+ reset_type = force ? "ForceOff" : "GracefulShutdown"
58
+ puts "#{force ? 'Force powering' : 'Gracefully shutting'} off system...".yellow
59
+
60
+ response = authenticated_request(
61
+ :post,
62
+ "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
63
+ body: { "ResetType": reset_type }.to_json,
64
+ headers: { 'Content-Type': 'application/json' }
65
+ )
66
+
67
+ if response.status.between?(200, 299)
68
+ puts "Power off command sent successfully.".green
69
+ return true
70
+ elsif !force
71
+ puts "Graceful shutdown failed, trying force off...".yellow
72
+
73
+ response = authenticated_request(
74
+ :post,
75
+ "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
76
+ body: { "ResetType": "ForceOff" }.to_json,
77
+ headers: { 'Content-Type': 'application/json' }
78
+ )
79
+
80
+ if response.status.between?(200, 299)
81
+ puts "Force power off command sent successfully.".green
82
+ return true
83
+ else
84
+ raise Error, "Failed to power off system: #{response.status} - #{response.body}"
85
+ end
86
+ else
87
+ raise Error, "Failed to force power off system: #{response.status} - #{response.body}"
88
+ end
89
+ end
90
+
91
+ def power_restart(force: false)
92
+ reset_type = force ? "ForceRestart" : "GracefulRestart"
93
+ puts "#{force ? 'Force restarting' : 'Gracefully restarting'} system...".yellow
94
+
95
+ response = authenticated_request(
96
+ :post,
97
+ "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
98
+ body: { "ResetType": reset_type }.to_json,
99
+ headers: { 'Content-Type': 'application/json' }
100
+ )
101
+
102
+ if response.status.between?(200, 299)
103
+ puts "Restart command sent successfully.".green
104
+ return true
105
+ elsif !force
106
+ puts "Graceful restart failed, trying force restart...".yellow
107
+
108
+ response = authenticated_request(
109
+ :post,
110
+ "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
111
+ body: { "ResetType": "ForceRestart" }.to_json,
112
+ headers: { 'Content-Type': 'application/json' }
113
+ )
114
+
115
+ if response.status.between?(200, 299)
116
+ puts "Force restart command sent successfully.".green
117
+ return true
118
+ else
119
+ raise Error, "Failed to restart system: #{response.status} - #{response.body}"
120
+ end
121
+ else
122
+ raise Error, "Failed to force restart system: #{response.status} - #{response.body}"
123
+ end
124
+ end
125
+
126
+ def power_cycle
127
+ puts "Power cycling system...".yellow
128
+
129
+ response = authenticated_request(
130
+ :post,
131
+ "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
132
+ body: { "ResetType": "PowerCycle" }.to_json,
133
+ headers: { 'Content-Type': 'application/json' }
134
+ )
135
+
136
+ if response.status.between?(200, 299)
137
+ puts "Power cycle command sent successfully.".green
138
+ return true
139
+ else
140
+ raise Error, "Failed to power cycle system: #{response.status} - #{response.body}"
141
+ end
142
+ end
143
+
144
+ def reset_type_allowed
145
+ response = authenticated_request(:get, "/redfish/v1/Systems/1")
146
+
147
+ if response.status == 200
148
+ begin
149
+ data = JSON.parse(response.body)
150
+
151
+ allowed_values = data.dig("Actions", "#ComputerSystem.Reset", "ResetType@Redfish.AllowableValues")
152
+
153
+ if allowed_values
154
+ puts "Allowed reset types:".green
155
+ allowed_values.each { |type| puts " - #{type}" }
156
+ return allowed_values
157
+ else
158
+ puts "Could not determine allowed reset types".yellow
159
+ return []
160
+ end
161
+ rescue JSON::ParserError
162
+ raise Error, "Failed to parse system response: #{response.body}"
163
+ end
164
+ else
165
+ raise Error, "Failed to get system info. Status code: #{response.status}"
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'faraday'
5
+ require 'base64'
6
+
7
+ module Supermicro
8
+ class Session
9
+ attr_reader :client, :x_auth_token, :session_id
10
+
11
+ include Debuggable
12
+
13
+ def initialize(client)
14
+ @client = client
15
+ @x_auth_token = nil
16
+ @session_id = nil
17
+ end
18
+
19
+ def connection
20
+ @connection ||= Faraday.new(url: client.base_url, ssl: { verify: client.verify_ssl }) do |faraday|
21
+ faraday.request :url_encoded
22
+ faraday.adapter Faraday.default_adapter
23
+ end
24
+ end
25
+
26
+ def create
27
+ debug "Creating Redfish session for #{client.host}", 1
28
+
29
+ payload = {
30
+ UserName: client.username,
31
+ Password: client.password
32
+ }.to_json
33
+
34
+ headers = {
35
+ 'Content-Type' => 'application/json',
36
+ 'Accept' => 'application/json'
37
+ }
38
+ headers['Host'] = client.host_header if client.host_header
39
+
40
+ begin
41
+ response = connection.post('/redfish/v1/SessionService/Sessions', payload, headers)
42
+
43
+ if response.status == 201
44
+ @x_auth_token = response.headers['x-auth-token']
45
+
46
+ if response.headers['location']
47
+ @session_id = response.headers['location'].split('/').last
48
+ end
49
+
50
+ begin
51
+ body = JSON.parse(response.body)
52
+ @session_id ||= body["Id"] if body.is_a?(Hash)
53
+ rescue JSON::ParserError
54
+ end
55
+
56
+ debug "Session created successfully. Token: #{@x_auth_token ? @x_auth_token[0..10] + '...' : 'nil'}", 1, :green
57
+ return true
58
+ else
59
+ debug "Failed to create session. Status: #{response.status}", 1, :red
60
+ debug "Response: #{response.body}", 2
61
+ return false
62
+ end
63
+ rescue Faraday::Error => e
64
+ debug "Connection error creating session: #{e.message}", 1, :red
65
+ return false
66
+ end
67
+ end
68
+
69
+ def delete
70
+ return unless @x_auth_token && @session_id
71
+
72
+ debug "Deleting session #{@session_id}", 1
73
+
74
+ headers = {
75
+ 'X-Auth-Token' => @x_auth_token,
76
+ 'Accept' => 'application/json'
77
+ }
78
+ headers['Host'] = client.host_header if client.host_header
79
+
80
+ begin
81
+ response = connection.delete("/redfish/v1/SessionService/Sessions/#{@session_id}", nil, headers)
82
+
83
+ if response.status == 204 || response.status == 200
84
+ debug "Session deleted successfully", 1, :green
85
+ @x_auth_token = nil
86
+ @session_id = nil
87
+ return true
88
+ else
89
+ debug "Failed to delete session. Status: #{response.status}", 1, :yellow
90
+ return false
91
+ end
92
+ rescue Faraday::Error => e
93
+ debug "Error deleting session: #{e.message}", 1, :yellow
94
+ return false
95
+ end
96
+ end
97
+
98
+ def valid?
99
+ return false unless @x_auth_token
100
+
101
+ headers = {
102
+ 'X-Auth-Token' => @x_auth_token,
103
+ 'Accept' => 'application/json'
104
+ }
105
+ headers['Host'] = client.host_header if client.host_header
106
+
107
+ begin
108
+ response = connection.get("/redfish/v1/SessionService/Sessions/#{@session_id}", nil, headers)
109
+ response.status == 200
110
+ rescue
111
+ false
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def verbosity
118
+ client.verbosity
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supermicro
4
+ class Spinner
5
+ SPINNERS = {
6
+ dots: {
7
+ frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
8
+ interval: 0.08
9
+ },
10
+ line: {
11
+ frames: ['-', '\\', '|', '/'],
12
+ interval: 0.1
13
+ },
14
+ arrow: {
15
+ frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
16
+ interval: 0.1
17
+ },
18
+ bounce: {
19
+ frames: ['⠁', '⠂', '⠄', '⡀', '⡈', '⡐', '⡠', '⣀', '⣁', '⣂', '⣄', '⣌', '⣔', '⣤', '⣥', '⣦', '⣮', '⣶', '⣷', '⣿', '⡿', '⠿', '⢟', '⠟', '⡛', '⠛', '⠫', '⢋', '⠋', '⠍', '⡉', '⠉', '⠑', '⠡', '⢁'],
20
+ interval: 0.08
21
+ },
22
+ bar: {
23
+ frames: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂'],
24
+ interval: 0.08
25
+ }
26
+ }
27
+
28
+ def initialize(message = "Working", type: :dots, color: :cyan)
29
+ @message = message
30
+ @type = type
31
+ @color = color
32
+ @running = false
33
+ @thread = nil
34
+ @current_frame = 0
35
+ @spinner_config = SPINNERS[@type] || SPINNERS[:dots]
36
+ @start_time = nil
37
+ @last_update = nil
38
+ @max_width = 0 # Track the maximum width we've printed
39
+ end
40
+
41
+ def start
42
+ return if @running
43
+ @running = true
44
+ @start_time = Time.now
45
+ @current_frame = 0
46
+
47
+ @thread = Thread.new do
48
+ while @running
49
+ render
50
+ sleep @spinner_config[:interval]
51
+ @current_frame = (@current_frame + 1) % @spinner_config[:frames].length
52
+ end
53
+ end
54
+ end
55
+
56
+ def update(message)
57
+ @message = message
58
+ @last_update = Time.now
59
+ # Immediately render to show the update
60
+ render if @running
61
+ end
62
+
63
+ def stop(final_message = nil, success: true)
64
+ return unless @running
65
+ @running = false
66
+ @thread&.join
67
+
68
+ # Clear the spinner line completely
69
+ print "\r\033[2K"
70
+
71
+ if final_message
72
+ icon = success ? "✓".green : "✗".red
73
+ elapsed = Time.now - @start_time
74
+ time_str = elapsed > 1 ? " (#{elapsed.round(1)}s)" : ""
75
+ puts "#{icon} #{final_message}#{time_str}"
76
+ end
77
+ end
78
+
79
+ def with_spinner
80
+ start
81
+ begin
82
+ yield self
83
+ ensure
84
+ stop
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def render
91
+ frame = @spinner_config[:frames][@current_frame]
92
+ elapsed = Time.now - @start_time
93
+ time_str = elapsed > 2 ? " (#{elapsed.round}s)" : ""
94
+
95
+ # Build the output string without color codes to calculate real width
96
+ text_content = "#{frame} #{@message}#{time_str}"
97
+ current_width = text_content.length
98
+
99
+ # Track maximum width seen
100
+ @max_width = current_width if current_width > @max_width
101
+
102
+ # Pad with spaces to clear any leftover text from longer previous messages
103
+ padding = @max_width > current_width ? " " * (@max_width - current_width) : ""
104
+
105
+ # Build the colored output
106
+ output = "\r#{frame.send(@color)} #{@message}#{time_str}#{padding}"
107
+
108
+ # Print without newline and flush immediately
109
+ print output
110
+ $stdout.flush
111
+ end
112
+ end
113
+
114
+ module SpinnerHelper
115
+ def with_spinner(message, type: :dots, color: :cyan, &block)
116
+ return yield if respond_to?(:verbosity) && verbosity > 0
117
+
118
+ spinner = Spinner.new(message, type: type, color: color)
119
+ spinner.start
120
+
121
+ begin
122
+ result = yield(spinner)
123
+ spinner.stop("#{message} - Complete", success: true)
124
+ result
125
+ rescue => e
126
+ spinner.stop("#{message} - Failed", success: false)
127
+ raise e
128
+ end
129
+ end
130
+
131
+ def show_progress(message, duration: nil, &block)
132
+ if duration
133
+ # Show progress bar if duration is known
134
+ with_progress_bar(message, duration, &block)
135
+ else
136
+ # Show spinner if duration is unknown
137
+ with_spinner(message, &block)
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def with_progress_bar(message, duration, &block)
144
+ return yield if respond_to?(:verbosity) && verbosity > 0
145
+
146
+ start_time = Time.now
147
+ width = 30
148
+
149
+ thread = Thread.new do
150
+ while true
151
+ elapsed = Time.now - start_time
152
+ progress = [elapsed / duration, 1.0].min
153
+ filled = (progress * width).round
154
+ bar = "█" * filled + "░" * (width - filled)
155
+ percent = (progress * 100).round
156
+
157
+ print "\r#{message}: [#{bar}] #{percent}%"
158
+ $stdout.flush
159
+
160
+ break if progress >= 1.0
161
+ sleep 0.1
162
+ end
163
+ end
164
+
165
+ begin
166
+ result = yield
167
+ thread.join
168
+ print "\r\e[K"
169
+ puts "✓ #{message} - Complete".green
170
+ result
171
+ rescue => e
172
+ thread.kill
173
+ print "\r\e[K"
174
+ puts "✗ #{message} - Failed".red
175
+ raise e
176
+ end
177
+ end
178
+ end
179
+ end