idrac 0.1.1 → 0.1.6

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: ea684775e5a6c176d4089ce5a88a0907170896a3818d715479ca53a58ee95d05
4
- data.tar.gz: dbe078ef0f036c27ecefbaeb454f63357561ecb0a9c9ee0f618f870b4e12b812
3
+ metadata.gz: 7f9e7e13774b28b39da48892cce8d6de1d6bfd2c4d226c54d584805b4dd886cd
4
+ data.tar.gz: 736cc4cb000a46b39a6faba61ffd03ceaf19fe40476171f3b911eb697d0045f0
5
5
  SHA512:
6
- metadata.gz: cfd4619dc968e53dd5ca1f773881c7c422b3b281ef423c6f626cae934ecce20368e2a4bccf9598a84a07e300c7540bf2c3bd0236f5ad2b3cb7c163f3f05a4357
7
- data.tar.gz: b6ee7e98a9ff60ba6886e01f8aac2d8c0da2c792e26bdab28409e532577660bbd8727e3c307af8351efb3b19cb2defcc91d37235c6c8a43e712b355d53bb2a43
6
+ metadata.gz: 35d7645d8b5d964ffc7b2dba7382fe0650015686b9eb781b6c7fda75aa6b6ab64b5944bb33c99fa38731309b18b3b232a3d8ed5c0553736e50ef43602a746495
7
+ data.tar.gz: 124ee6ee23aef24e7908d4765c2ccf01ca33bcbdf42b0ae7e36175c3b48db7aa0fe400578d6437bd22403959c1a6ccb9177254ef4c34001320f98707d5c46320
data/README.md CHANGED
@@ -1,24 +1,96 @@
1
- # Idrac
1
+ # IDRAC
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ A Ruby client for the Dell iDRAC API. This gem provides a command-line interface and a Ruby API for interacting with Dell iDRAC servers.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/idrac`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## Features
6
+
7
+ - Take screenshots of the iDRAC console
8
+ - Update firmware using Dell's catalog
9
+ - Check for firmware updates
10
+ - Interactive firmware update process
6
11
 
7
12
  ## Installation
8
13
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'idrac'
18
+ ```
10
19
 
11
- Install the gem and add to the application's Gemfile by executing:
20
+ And then execute:
12
21
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
22
+ $ bundle install
14
23
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
24
+ Or install it yourself as:
16
25
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
26
+ $ gem install idrac
18
27
 
19
28
  ## Usage
20
29
 
21
- TODO: Write usage instructions here
30
+ ### Command Line Interface
31
+
32
+ The gem provides a command-line interface for interacting with iDRAC servers:
33
+
34
+ ```bash
35
+ # Take a screenshot of the iDRAC console
36
+ idrac screenshot --host=192.168.1.100 --username=root --password=calvin
37
+ # Specify a custom output filename
38
+ idrac screenshot --host=192.168.1.100 --username=root --password=calvin --output=my_screenshot.png
39
+
40
+ # Download the Dell firmware catalog
41
+ idrac firmware:catalog --host=192.168.1.100 --username=root --password=calvin
42
+
43
+ # Check firmware status and available updates
44
+ idrac firmware:status --host=192.168.1.100 --username=root --password=calvin
45
+
46
+ # Update firmware using a specific file
47
+ idrac firmware:update /path/to/firmware.exe --host=192.168.1.100 --username=root --password=calvin
48
+
49
+ # Interactive firmware update
50
+ idrac firmware:interactive --host=192.168.1.100 --username=root --password=calvin
51
+ ```
52
+
53
+ ### Ruby API
54
+
55
+ ```ruby
56
+ require 'idrac'
57
+
58
+ # Create a client
59
+ client = IDRAC.new(
60
+ host: '192.168.1.100',
61
+ username: 'root',
62
+ password: 'calvin'
63
+ )
64
+
65
+ # Take a screenshot (using the client convenience method)
66
+ filename = client.screenshot
67
+ puts "Screenshot saved to: #{filename}"
68
+
69
+ # Or use the Screenshot class directly for more control
70
+ screenshot = IDRAC::Screenshot.new(client)
71
+ filename = screenshot.capture
72
+ puts "Screenshot saved to: #{filename}"
73
+
74
+ # Firmware operations
75
+ firmware = IDRAC::Firmware.new(client)
76
+
77
+ # Download catalog
78
+ catalog_path = firmware.download_catalog
79
+
80
+ # Get system inventory
81
+ inventory = firmware.get_system_inventory
82
+ puts "Service Tag: #{inventory[:system][:service_tag]}"
83
+
84
+ # Check for updates
85
+ updates = firmware.check_updates(catalog_path)
86
+ updates.each do |update|
87
+ puts "#{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
88
+ end
89
+
90
+ # Update firmware
91
+ job_id = firmware.update('/path/to/firmware.exe', wait: true)
92
+ puts "Update completed with job ID: #{job_id}"
93
+ ```
22
94
 
23
95
  ## Development
24
96
 
@@ -28,4 +100,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
28
100
 
29
101
  ## Contributing
30
102
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/idrac.
103
+ Bug reports and pull requests are welcome on GitHub at https://github.com/usiegj00/idrac.
data/Rakefile CHANGED
@@ -6,3 +6,12 @@ require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
+
10
+ # Add a task that tags and pushes to the repository and builds
11
+ # the gem and pushes it to rubygems.org.
12
+ # Depend on the build task to ensure the gem is up to date.
13
+ task :release => [:build] do
14
+ system "git tag v#{Idrac::VERSION}"
15
+ system "git push --tags"
16
+ system "gem push pkg/idrac-#{Idrac::VERSION}.gem"
17
+ end
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "idrac"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/idrac ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Attempt to load bundler/setup, but don't fail if it's not available
4
+ begin
5
+ require "bundler/setup"
6
+ rescue LoadError
7
+ # Continue without bundler
8
+ end
9
+
10
+ # Load required gems directly
11
+ require "thor"
12
+ require "idrac"
13
+
14
+ module IDRAC
15
+ class CLI < Thor
16
+ class_option :host, type: :string, required: true, desc: "iDRAC host address"
17
+ class_option :username, type: :string, required: true, desc: "iDRAC username"
18
+ class_option :password, type: :string, required: true, desc: "iDRAC password"
19
+ class_option :port, type: :numeric, default: 443, desc: "iDRAC port"
20
+ class_option :no_ssl, type: :boolean, default: false, desc: "Disable SSL"
21
+ class_option :no_verify_ssl, type: :boolean, default: false, desc: "Disable SSL verification"
22
+
23
+ desc "firmware:update PATH", "Update firmware using the specified file"
24
+ method_option :wait, type: :boolean, default: true, desc: "Wait for the update to complete"
25
+ method_option :timeout, type: :numeric, default: 3600, desc: "Timeout in seconds when waiting"
26
+ def firmware_update(path)
27
+ client = create_client
28
+ firmware = IDRAC::Firmware.new(client)
29
+
30
+ begin
31
+ job_id = firmware.update(path, wait: options[:wait], timeout: options[:timeout])
32
+ puts "Firmware update initiated with job ID: #{job_id}"
33
+ rescue IDRAC::Error => e
34
+ puts "Error: #{e.message}"
35
+ exit 1
36
+ ensure
37
+ client.logout
38
+ end
39
+ end
40
+
41
+ desc "firmware:catalog [DIRECTORY]", "Download Dell firmware catalog"
42
+ def firmware_catalog(directory = nil)
43
+ client = create_client
44
+ firmware = IDRAC::Firmware.new(client)
45
+
46
+ begin
47
+ catalog_path = firmware.download_catalog(directory)
48
+ puts "Catalog downloaded to: #{catalog_path}"
49
+ rescue IDRAC::Error => e
50
+ puts "Error: #{e.message}"
51
+ exit 1
52
+ ensure
53
+ client.logout
54
+ end
55
+ end
56
+
57
+ desc "firmware:status", "Show current firmware status and available updates"
58
+ method_option :catalog, type: :string, desc: "Path to existing catalog file"
59
+ def firmware_status
60
+ client = create_client
61
+ firmware = IDRAC::Firmware.new(client)
62
+
63
+ begin
64
+ # Get system inventory
65
+ inventory = firmware.get_system_inventory
66
+
67
+ puts "System Information:"
68
+ puts " Model: #{inventory[:system][:model]}"
69
+ puts " Manufacturer: #{inventory[:system][:manufacturer]}"
70
+ puts " Service Tag: #{inventory[:system][:service_tag]}"
71
+ puts " BIOS Version: #{inventory[:system][:bios_version]}"
72
+
73
+ puts "\nInstalled Firmware:"
74
+ inventory[:firmware].each do |fw|
75
+ puts " #{fw[:name]}: #{fw[:version]} (#{fw[:updateable] ? 'Updateable' : 'Not Updateable'})"
76
+ end
77
+
78
+ # Check for updates if catalog is available
79
+ if options[:catalog] || File.exist?(File.join(Dir.pwd, "Catalog.xml"))
80
+ catalog_path = options[:catalog] || File.join(Dir.pwd, "Catalog.xml")
81
+ puts "\nChecking for updates using catalog: #{catalog_path}"
82
+
83
+ updates = firmware.check_updates(catalog_path)
84
+
85
+ if updates.empty?
86
+ puts "No updates available."
87
+ else
88
+ puts "\nAvailable Updates:"
89
+ updates.each do |update|
90
+ puts " #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
91
+ end
92
+ end
93
+ else
94
+ puts "\nTo check for updates, download the catalog first with 'idrac firmware:catalog'"
95
+ end
96
+ rescue IDRAC::Error => e
97
+ puts "Error: #{e.message}"
98
+ exit 1
99
+ ensure
100
+ client.logout
101
+ end
102
+ end
103
+
104
+ desc "firmware:interactive", "Interactive firmware update"
105
+ method_option :catalog, type: :string, desc: "Path to existing catalog file"
106
+ def firmware_interactive
107
+ client = create_client
108
+ firmware = IDRAC::Firmware.new(client)
109
+
110
+ begin
111
+ catalog_path = options[:catalog]
112
+
113
+ # If no catalog specified, check if one exists in current directory
114
+ if catalog_path.nil? && File.exist?(File.join(Dir.pwd, "Catalog.xml"))
115
+ catalog_path = File.join(Dir.pwd, "Catalog.xml")
116
+ end
117
+
118
+ # If still no catalog, download it
119
+ if catalog_path.nil?
120
+ puts "No catalog found. Downloading..."
121
+ catalog_path = firmware.download_catalog
122
+ end
123
+
124
+ firmware.interactive_update(catalog_path)
125
+ rescue IDRAC::Error => e
126
+ puts "Error: #{e.message}"
127
+ exit 1
128
+ ensure
129
+ client.logout
130
+ end
131
+ end
132
+
133
+ desc "screenshot", "Take a screenshot of the current iDRAC console"
134
+ method_option :output, type: :string, desc: "Output filename (default: idrac_screenshot_timestamp.png)"
135
+ def screenshot
136
+ client = create_client
137
+
138
+ begin
139
+ # Create a Screenshot instance directly
140
+ screenshot = IDRAC::Screenshot.new(client)
141
+ filename = screenshot.capture
142
+
143
+ # Rename the file if output option is provided
144
+ if options[:output]
145
+ new_filename = options[:output]
146
+ File.rename(filename, new_filename)
147
+ filename = new_filename
148
+ end
149
+
150
+ puts "Screenshot saved to: #{filename}"
151
+ rescue IDRAC::Error => e
152
+ puts "Error: #{e.message}"
153
+ exit 1
154
+ ensure
155
+ client.logout
156
+ end
157
+ end
158
+
159
+ map "firmware:update" => :firmware_update
160
+ map "firmware:catalog" => :firmware_catalog
161
+ map "firmware:status" => :firmware_status
162
+ map "firmware:interactive" => :firmware_interactive
163
+
164
+ private
165
+
166
+ def create_client
167
+ IDRAC::Client.new(
168
+ host: options[:host],
169
+ username: options[:username],
170
+ password: options[:password],
171
+ port: options[:port],
172
+ use_ssl: !options[:no_ssl],
173
+ verify_ssl: !options[:no_verify_ssl]
174
+ )
175
+ end
176
+ end
177
+ end
178
+
179
+ IDRAC::CLI.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/idrac.gemspec CHANGED
@@ -4,14 +4,15 @@ require_relative "lib/idrac/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "idrac"
7
- spec.version = Idrac::VERSION
7
+ spec.version = IDRAC::VERSION
8
8
  spec.authors = ["Jonathan Siegel"]
9
9
  spec.email = ["<248302+usiegj00@users.noreply.github.com>"]
10
10
 
11
11
  spec.summary = "API Client for Dell iDRAC"
12
12
  spec.description = "A Ruby client for the Dell iDRAC API"
13
13
  spec.homepage = "http://github.com"
14
- spec.required_ruby_version = ">= 3.3.0" # Feel free to test and update.
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2.0" # Updated to support Ruby 3.2.x
15
16
 
16
17
  # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
17
18
 
@@ -24,22 +25,26 @@ Gem::Specification.new do |spec|
24
25
  spec.files = Dir.chdir(__dir__) do
25
26
  `git ls-files -z`.split("\x0").reject do |f|
26
27
  (File.expand_path(f) == __FILE__) ||
27
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
28
+ f.start_with?(*%w[test/ spec/ features/ .git .circleci appveyor Gemfile])
28
29
  end
29
30
  end
30
- spec.bindir = "exe"
31
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.bindir = "bin"
32
+ spec.executables = ["idrac"]
32
33
  spec.require_paths = ["lib"]
33
34
 
34
- # Uncomment to register a new dependency of your gem
35
- spec.add_dependency "httparty"
36
- spec.add_dependency "nokogiri"
37
-
38
- # Add dev dependencies:
39
- spec.add_development_dependency "bundler"
40
- spec.add_development_dependency "rake"
41
- spec.add_development_dependency "rspec"
42
- spec.add_development_dependency "debug"
35
+ # Dependencies - Using more flexible version constraints
36
+ spec.add_dependency "httparty", ">= 0.21.0", "< 0.22.0"
37
+ spec.add_dependency "nokogiri", ">= 1.15.0", "< 1.19.0"
38
+ spec.add_dependency "faraday", ">= 2.7.0", "< 2.8.0"
39
+ spec.add_dependency "faraday-multipart", ">= 1.0.0", "< 1.1.0"
40
+ spec.add_dependency "thor", ">= 1.2.0", "< 1.4.0"
41
+ spec.add_dependency "base64", "~> 0.1", ">= 0.1.0"
42
+
43
+ # Development dependencies
44
+ spec.add_development_dependency "bundler", "~> 2.4", ">= 2.4.0"
45
+ spec.add_development_dependency "rake", "~> 13.0"
46
+ spec.add_development_dependency "rspec", "~> 3.12"
47
+ spec.add_development_dependency "debug", "~> 1.8"
43
48
 
44
49
  # For more information and examples about making a new gem, check out our
45
50
  # guide at: https://bundler.io/guides/creating_gem.html
data/lib/idrac/client.rb CHANGED
@@ -1,41 +1,91 @@
1
- module Idrac
1
+ require 'faraday'
2
+ require 'faraday/multipart'
3
+ require 'nokogiri'
4
+ require 'base64'
5
+ require 'uri'
6
+ require 'httparty'
7
+
8
+ module IDRAC
2
9
  class Client
3
- def initialize(url:)
4
- @url = url
10
+ attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl
11
+
12
+ def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true)
13
+ @host = host
14
+ @username = username
15
+ @password = password
16
+ @port = port
17
+ @use_ssl = use_ssl
18
+ @verify_ssl = verify_ssl
19
+ @session_id = nil
5
20
  @cookies = nil
6
21
  end
7
22
 
8
- def login(user: 'root', password: 'calvin')
9
- login_response = HTTParty.post(
10
- "#{@url}/data/login",
11
- body: { user: user, password: password },
12
- headers: {
13
- "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
14
- "Accept-Encoding" => "deflate, gzip"
15
- },
16
- verify: false
17
- )
18
- @cookies = login_response.headers['set-cookie']
19
- xml_doc = Nokogiri::XML(login_response.body)
20
-
21
- # Extract the forwardUrl from the XML
22
- error_message = xml_doc.at_xpath('//errorMsg')&.text
23
- # Error Message: The maximum number of user sessions has been reached!
24
- if error_message && !error_message.empty?
25
- raise "Error Message: #{error_message}"
23
+ def connection
24
+ @connection ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday|
25
+ faraday.request :multipart
26
+ faraday.request :url_encoded
27
+ faraday.adapter Faraday.default_adapter
28
+ end
29
+ end
30
+
31
+ def login
32
+ response = connection.post('/data/login') do |req|
33
+ req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
34
+ req.body = "user=#{username}&password=#{password}"
35
+ end
36
+
37
+ if response.status == 200
38
+ # Extract session ID from response
39
+ doc = Nokogiri::HTML(response.body)
40
+ @session_id = doc.at_css('script:contains("_this.SID")').text.match(/_this\.SID\s*=\s*"([^"]+)"/)[1] rescue nil
41
+
42
+ # Store cookies for screenshot functionality
43
+ @cookies = response.headers['set-cookie']
44
+
45
+ # Extract forward URL for screenshot functionality
46
+ xml_doc = Nokogiri::XML(response.body)
47
+ error_message = xml_doc.at_xpath('//errorMsg')&.text
48
+
49
+ if error_message && !error_message.empty?
50
+ raise Error, "Error Message: #{error_message}"
51
+ end
52
+
53
+ forward_url = xml_doc.xpath('//forwardUrl').text
54
+ return forward_url
55
+ else
56
+ raise Error, "Login failed with status #{response.status}: #{response.body}"
26
57
  end
27
- forward_url = xml_doc.xpath('//forwardUrl').text
28
58
  end
29
59
 
30
60
  def logout
31
- path = "data/logout"
32
- res = get(path: path)
33
- raise "Invalid login." unless res.code.between?(200, 299)
61
+ return unless @session_id
62
+
63
+ response = connection.get('/data/logout') do |req|
64
+ req.headers['Cookie'] = "sessionid=#{@session_id}"
65
+ end
66
+
67
+ @session_id = nil
68
+ @cookies = nil
69
+ response.status == 200
34
70
  end
71
+
72
+ def authenticated_request(method, path, options = {})
73
+ login unless @session_id
74
+
75
+ options[:headers] ||= {}
76
+ options[:headers]['Cookie'] = "sessionid=#{@session_id}"
77
+
78
+ response = connection.send(method, path, options[:params]) do |req|
79
+ req.headers.merge!(options[:headers])
80
+ req.body = options[:body] if options[:body]
81
+ end
35
82
 
83
+ response
84
+ end
85
+
36
86
  def get(path:, headers: {})
37
87
  response = HTTParty.get(
38
- "#{@url}/#{path}",
88
+ "#{base_url}/#{path}",
39
89
  headers: {
40
90
  "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
41
91
  "Accept-Encoding" => "deflate, gzip",
@@ -44,5 +94,16 @@ module Idrac
44
94
  verify: false
45
95
  )
46
96
  end
97
+
98
+ def screenshot
99
+ # Create a Screenshot instance and capture a screenshot
100
+ screenshot_instance = Screenshot.new(self)
101
+ screenshot_instance.capture
102
+ end
103
+
104
+ def base_url
105
+ protocol = use_ssl ? 'https' : 'http'
106
+ "#{protocol}://#{host}:#{port}"
107
+ end
47
108
  end
48
109
  end
@@ -0,0 +1,366 @@
1
+ require 'tempfile'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'json'
5
+ require 'nokogiri'
6
+ require 'fileutils'
7
+ require 'securerandom'
8
+
9
+ module IDRAC
10
+ class Firmware
11
+ attr_reader :client
12
+
13
+ CATALOG_URL = "https://downloads.dell.com/catalog/Catalog.xml.gz"
14
+
15
+ def initialize(client)
16
+ @client = client
17
+ end
18
+
19
+ def update(firmware_path, options = {})
20
+ # Validate firmware file exists
21
+ unless File.exist?(firmware_path)
22
+ raise Error, "Firmware file not found: #{firmware_path}"
23
+ end
24
+
25
+ # Login to iDRAC
26
+ client.login unless client.instance_variable_get(:@session_id)
27
+
28
+ # Upload firmware file
29
+ job_id = upload_firmware(firmware_path)
30
+
31
+ # Check if we should wait for the update to complete
32
+ if options[:wait]
33
+ wait_for_job_completion(job_id, options[:timeout] || 3600)
34
+ end
35
+
36
+ job_id
37
+ end
38
+
39
+ def download_catalog(output_dir = nil)
40
+ output_dir ||= Dir.pwd
41
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
42
+
43
+ catalog_gz_path = File.join(output_dir, "Catalog.xml.gz")
44
+ catalog_path = File.join(output_dir, "Catalog.xml")
45
+
46
+ puts "Downloading Dell catalog from #{CATALOG_URL}..."
47
+
48
+ uri = URI.parse(CATALOG_URL)
49
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
50
+ request = Net::HTTP::Get.new(uri)
51
+ http.request(request) do |response|
52
+ if response.code == "200"
53
+ File.open(catalog_gz_path, 'wb') do |file|
54
+ response.read_body do |chunk|
55
+ file.write(chunk)
56
+ end
57
+ end
58
+ else
59
+ raise Error, "Failed to download catalog: #{response.code} #{response.message}"
60
+ end
61
+ end
62
+ end
63
+
64
+ puts "Extracting catalog..."
65
+ system("gunzip -f #{catalog_gz_path}")
66
+
67
+ if File.exist?(catalog_path)
68
+ puts "Catalog downloaded and extracted to #{catalog_path}"
69
+ return catalog_path
70
+ else
71
+ raise Error, "Failed to extract catalog"
72
+ end
73
+ end
74
+
75
+ def get_system_inventory
76
+ puts "Retrieving system inventory..."
77
+
78
+ # Get basic system information
79
+ system_uri = URI.parse("#{client.base_url}/redfish/v1/Systems/System.Embedded.1")
80
+ system_response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
81
+
82
+ if system_response.status != 200
83
+ raise Error, "Failed to get system information: #{system_response.status}"
84
+ end
85
+
86
+ system_data = JSON.parse(system_response.body)
87
+
88
+ # Get firmware inventory
89
+ firmware_uri = URI.parse("#{client.base_url}/redfish/v1/UpdateService/FirmwareInventory")
90
+ firmware_response = client.authenticated_request(:get, "/redfish/v1/UpdateService/FirmwareInventory")
91
+
92
+ if firmware_response.status != 200
93
+ raise Error, "Failed to get firmware inventory: #{firmware_response.status}"
94
+ end
95
+
96
+ firmware_data = JSON.parse(firmware_response.body)
97
+
98
+ # Get detailed firmware information for each component
99
+ firmware_inventory = []
100
+
101
+ if firmware_data['Members'] && firmware_data['Members'].is_a?(Array)
102
+ firmware_data['Members'].each do |member|
103
+ if member['@odata.id']
104
+ component_uri = member['@odata.id']
105
+ component_response = client.authenticated_request(:get, component_uri)
106
+
107
+ if component_response.status == 200
108
+ component_data = JSON.parse(component_response.body)
109
+ firmware_inventory << {
110
+ name: component_data['Name'],
111
+ id: component_data['Id'],
112
+ version: component_data['Version'],
113
+ updateable: component_data['Updateable'] || false,
114
+ status: component_data['Status'] ? component_data['Status']['State'] : 'Unknown'
115
+ }
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ {
122
+ system: {
123
+ model: system_data['Model'],
124
+ manufacturer: system_data['Manufacturer'],
125
+ serial_number: system_data['SerialNumber'],
126
+ part_number: system_data['PartNumber'],
127
+ bios_version: system_data['BiosVersion'],
128
+ service_tag: system_data['SKU']
129
+ },
130
+ firmware: firmware_inventory
131
+ }
132
+ end
133
+
134
+ def check_updates(catalog_path = nil)
135
+ # Download catalog if not provided
136
+ catalog_path ||= download_catalog
137
+
138
+ # Get system inventory
139
+ inventory = get_system_inventory
140
+
141
+ # Parse catalog
142
+ catalog_doc = File.open(catalog_path) { |f| Nokogiri::XML(f) }
143
+
144
+ # Extract service tag
145
+ service_tag = inventory[:system][:service_tag]
146
+
147
+ puts "Checking updates for system with service tag: #{service_tag}"
148
+
149
+ # Find applicable updates
150
+ updates = []
151
+
152
+ # Get current firmware versions
153
+ current_versions = {}
154
+ inventory[:firmware].each do |fw|
155
+ current_versions[fw[:name]] = fw[:version]
156
+ end
157
+
158
+ # Find matching components in catalog
159
+ catalog_doc.xpath('//SoftwareComponent').each do |component|
160
+ name = component.at_xpath('Name')&.text
161
+ version = component.at_xpath('Version')&.text
162
+ path = component.at_xpath('Path')&.text
163
+ component_type = component.at_xpath('ComponentType')&.text
164
+
165
+ # Check if this component matches any of our firmware
166
+ inventory[:firmware].each do |fw|
167
+ if fw[:name].include?(name) || name.include?(fw[:name])
168
+ current_version = fw[:version]
169
+
170
+ # Simple version comparison (this could be improved)
171
+ if version != current_version
172
+ updates << {
173
+ name: name,
174
+ current_version: current_version,
175
+ available_version: version,
176
+ path: path,
177
+ component_type: component_type,
178
+ download_url: "https://downloads.dell.com/#{path}"
179
+ }
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ updates
186
+ end
187
+
188
+ def interactive_update(catalog_path = nil)
189
+ updates = check_updates(catalog_path)
190
+
191
+ if updates.empty?
192
+ puts "No updates available for your system."
193
+ return
194
+ end
195
+
196
+ puts "\nAvailable updates:"
197
+ updates.each_with_index do |update, index|
198
+ puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
199
+ end
200
+
201
+ puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):"
202
+ choice = STDIN.gets.chomp
203
+
204
+ return if choice.downcase == 'q'
205
+
206
+ selected_updates = if choice.downcase == 'all'
207
+ updates
208
+ else
209
+ index = choice.to_i - 1
210
+ if index >= 0 && index < updates.size
211
+ [updates[index]]
212
+ else
213
+ puts "Invalid selection."
214
+ return
215
+ end
216
+ end
217
+
218
+ selected_updates.each do |update|
219
+ puts "Downloading #{update[:name]} version #{update[:available_version]}..."
220
+
221
+ # Create temp directory
222
+ temp_dir = Dir.mktmpdir
223
+
224
+ begin
225
+ # Download the update
226
+ update_filename = File.basename(update[:path])
227
+ update_path = File.join(temp_dir, update_filename)
228
+
229
+ uri = URI.parse(update[:download_url])
230
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
231
+ request = Net::HTTP::Get.new(uri)
232
+ http.request(request) do |response|
233
+ if response.code == "200"
234
+ File.open(update_path, 'wb') do |file|
235
+ response.read_body do |chunk|
236
+ file.write(chunk)
237
+ end
238
+ end
239
+ else
240
+ puts "Failed to download update: #{response.code} #{response.message}"
241
+ next
242
+ end
243
+ end
244
+ end
245
+
246
+ puts "Installing #{update[:name]} version #{update[:available_version]}..."
247
+ job_id = update(update_path, wait: true)
248
+ puts "Update completed with job ID: #{job_id}"
249
+
250
+ ensure
251
+ # Clean up temp directory
252
+ FileUtils.remove_entry(temp_dir)
253
+ end
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def upload_firmware(firmware_path)
260
+ puts "Uploading firmware file: #{firmware_path}"
261
+
262
+ # Get the HttpPushUri from UpdateService
263
+ update_service_response = client.authenticated_request(:get, "/redfish/v1/UpdateService")
264
+ update_service_data = JSON.parse(update_service_response.body)
265
+
266
+ http_push_uri = update_service_data['HttpPushUri']
267
+ if http_push_uri.nil?
268
+ http_push_uri = "/redfish/v1/UpdateService/FirmwareInventory"
269
+ puts "HttpPushUri not found, using default: #{http_push_uri}"
270
+ else
271
+ puts "Found HttpPushUri: #{http_push_uri}"
272
+ end
273
+
274
+ # Get the ETag for the HttpPushUri
275
+ etag_response = client.authenticated_request(:get, http_push_uri)
276
+ etag = etag_response.headers['etag']
277
+
278
+ puts "Got ETag: #{etag}"
279
+
280
+ # Create a boundary for multipart/form-data
281
+ boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
282
+
283
+ # Read the file content
284
+ file_content = File.binread(firmware_path)
285
+ filename = File.basename(firmware_path)
286
+
287
+ # Create the multipart body
288
+ post_body = []
289
+ post_body << "--#{boundary}\r\n"
290
+ post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
291
+ post_body << "Content-Type: application/octet-stream\r\n\r\n"
292
+ post_body << file_content
293
+ post_body << "\r\n--#{boundary}--\r\n"
294
+
295
+ # Upload the firmware
296
+ response = client.authenticated_request(
297
+ :post,
298
+ http_push_uri,
299
+ {
300
+ headers: {
301
+ 'Content-Type' => "multipart/form-data; boundary=#{boundary}",
302
+ 'If-Match' => etag
303
+ },
304
+ body: post_body.join
305
+ }
306
+ )
307
+
308
+ if response.status < 200 || response.status >= 300
309
+ raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
310
+ end
311
+
312
+ # Extract job ID from response
313
+ response_data = JSON.parse(response.body)
314
+ job_id = response_data['Id'] || response_data['TaskId']
315
+
316
+ if job_id.nil?
317
+ raise Error, "Failed to extract job ID from firmware upload response"
318
+ end
319
+
320
+ puts "Firmware update job created with ID: #{job_id}"
321
+ job_id
322
+ end
323
+
324
+ def wait_for_job_completion(job_id, timeout)
325
+ puts "Waiting for firmware update job #{job_id} to complete..."
326
+
327
+ start_time = Time.now
328
+ loop do
329
+ status = get_job_status(job_id)
330
+
331
+ case status
332
+ when 'Completed'
333
+ puts "Firmware update completed successfully"
334
+ return true
335
+ when 'Failed'
336
+ raise Error, "Firmware update job failed"
337
+ when 'Scheduled', 'Running', 'Downloading', 'Pending'
338
+ # Job still in progress
339
+ else
340
+ puts "Unknown job status: #{status}"
341
+ end
342
+
343
+ if Time.now - start_time > timeout
344
+ raise Error, "Firmware update timed out after #{timeout} seconds"
345
+ end
346
+
347
+ # Wait before checking again
348
+ sleep 10
349
+ end
350
+ end
351
+
352
+ def get_job_status(job_id)
353
+ response = client.authenticated_request(
354
+ :get,
355
+ "/redfish/v1/TaskService/Tasks/#{job_id}"
356
+ )
357
+
358
+ if response.status != 200
359
+ raise Error, "Failed to get job status with status #{response.status}: #{response.body}"
360
+ end
361
+
362
+ response_data = JSON.parse(response.body)
363
+ response_data['TaskState'] || 'Unknown'
364
+ end
365
+ end
366
+ end
data/lib/idrac/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Idrac
4
- VERSION = "0.1.1"
3
+ module IDRAC
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/idrac.rb CHANGED
@@ -2,17 +2,29 @@
2
2
 
3
3
  require 'httparty'
4
4
  require 'nokogiri'
5
+ require 'faraday'
6
+ require 'faraday/multipart'
7
+ require 'base64'
8
+ require 'uri'
5
9
  # If dev, required debug
6
10
  require 'debug' if ENV['RUBY_ENV'] == 'development'
7
11
 
8
12
  require_relative "idrac/version"
9
13
  require_relative "idrac/client"
10
14
  require_relative "idrac/screenshot"
15
+ require_relative "idrac/firmware"
11
16
 
12
- module Idrac
17
+ module IDRAC
13
18
  class Error < StandardError; end
14
- # Your code goes here...
15
- def self.new(url:)
16
- Client.new(url:)
19
+
20
+ def self.new(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true)
21
+ Client.new(
22
+ host: host,
23
+ username: username,
24
+ password: password,
25
+ port: port,
26
+ use_ssl: use_ssl,
27
+ verify_ssl: verify_ssl
28
+ )
17
29
  end
18
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-08 00:00:00.000000000 Z
11
+ date: 2025-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -16,102 +16,205 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.21.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 0.22.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '0'
29
+ version: 0.21.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.22.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: nokogiri
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
- version: '0'
39
+ version: 1.15.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: 1.19.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 1.15.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 1.19.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: faraday
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.7.0
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: 2.8.0
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 2.7.0
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: 2.8.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: faraday-multipart
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 1.0.0
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.1.0
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 1.0.0
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: 1.1.0
93
+ - !ruby/object:Gem::Dependency
94
+ name: thor
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 1.2.0
100
+ - - "<"
101
+ - !ruby/object:Gem::Version
102
+ version: 1.4.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 1.2.0
110
+ - - "<"
111
+ - !ruby/object:Gem::Version
112
+ version: 1.4.0
113
+ - !ruby/object:Gem::Dependency
114
+ name: base64
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '0.1'
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 0.1.0
34
123
  type: :runtime
35
124
  prerelease: false
36
125
  version_requirements: !ruby/object:Gem::Requirement
37
126
  requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '0.1'
38
130
  - - ">="
39
131
  - !ruby/object:Gem::Version
40
- version: '0'
132
+ version: 0.1.0
41
133
  - !ruby/object:Gem::Dependency
42
134
  name: bundler
43
135
  requirement: !ruby/object:Gem::Requirement
44
136
  requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '2.4'
45
140
  - - ">="
46
141
  - !ruby/object:Gem::Version
47
- version: '0'
142
+ version: 2.4.0
48
143
  type: :development
49
144
  prerelease: false
50
145
  version_requirements: !ruby/object:Gem::Requirement
51
146
  requirements:
147
+ - - "~>"
148
+ - !ruby/object:Gem::Version
149
+ version: '2.4'
52
150
  - - ">="
53
151
  - !ruby/object:Gem::Version
54
- version: '0'
152
+ version: 2.4.0
55
153
  - !ruby/object:Gem::Dependency
56
154
  name: rake
57
155
  requirement: !ruby/object:Gem::Requirement
58
156
  requirements:
59
- - - ">="
157
+ - - "~>"
60
158
  - !ruby/object:Gem::Version
61
- version: '0'
159
+ version: '13.0'
62
160
  type: :development
63
161
  prerelease: false
64
162
  version_requirements: !ruby/object:Gem::Requirement
65
163
  requirements:
66
- - - ">="
164
+ - - "~>"
67
165
  - !ruby/object:Gem::Version
68
- version: '0'
166
+ version: '13.0'
69
167
  - !ruby/object:Gem::Dependency
70
168
  name: rspec
71
169
  requirement: !ruby/object:Gem::Requirement
72
170
  requirements:
73
- - - ">="
171
+ - - "~>"
74
172
  - !ruby/object:Gem::Version
75
- version: '0'
173
+ version: '3.12'
76
174
  type: :development
77
175
  prerelease: false
78
176
  version_requirements: !ruby/object:Gem::Requirement
79
177
  requirements:
80
- - - ">="
178
+ - - "~>"
81
179
  - !ruby/object:Gem::Version
82
- version: '0'
180
+ version: '3.12'
83
181
  - !ruby/object:Gem::Dependency
84
182
  name: debug
85
183
  requirement: !ruby/object:Gem::Requirement
86
184
  requirements:
87
- - - ">="
185
+ - - "~>"
88
186
  - !ruby/object:Gem::Version
89
- version: '0'
187
+ version: '1.8'
90
188
  type: :development
91
189
  prerelease: false
92
190
  version_requirements: !ruby/object:Gem::Requirement
93
191
  requirements:
94
- - - ">="
192
+ - - "~>"
95
193
  - !ruby/object:Gem::Version
96
- version: '0'
194
+ version: '1.8'
97
195
  description: A Ruby client for the Dell iDRAC API
98
196
  email:
99
197
  - "<248302+usiegj00@users.noreply.github.com>"
100
- executables: []
198
+ executables:
199
+ - idrac
101
200
  extensions: []
102
201
  extra_rdoc_files: []
103
202
  files:
104
203
  - ".rspec"
105
204
  - README.md
106
205
  - Rakefile
206
+ - bin/console
207
+ - bin/idrac
208
+ - bin/setup
107
209
  - idrac.gemspec
108
210
  - lib/idrac.rb
109
211
  - lib/idrac/client.rb
110
- - lib/idrac/screenshot.rb
212
+ - lib/idrac/firmware.rb
111
213
  - lib/idrac/version.rb
112
214
  - sig/idrac.rbs
113
215
  homepage: http://github.com
114
- licenses: []
216
+ licenses:
217
+ - MIT
115
218
  metadata:
116
219
  homepage_uri: http://github.com
117
220
  post_install_message:
@@ -122,14 +225,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
225
  requirements:
123
226
  - - ">="
124
227
  - !ruby/object:Gem::Version
125
- version: 3.3.0
228
+ version: 3.2.0
126
229
  required_rubygems_version: !ruby/object:Gem::Requirement
127
230
  requirements:
128
231
  - - ">="
129
232
  - !ruby/object:Gem::Version
130
233
  version: '0'
131
234
  requirements: []
132
- rubygems_version: 3.5.3
235
+ rubygems_version: 3.4.19
133
236
  signing_key:
134
237
  specification_version: 4
135
238
  summary: API Client for Dell iDRAC
@@ -1,24 +0,0 @@
1
- module Idrac
2
- # Reverse engineered the screenshot functionality in iDRAC 2.X
3
- # Emulates the manual screenshot functionality in the web UI.
4
- class Client
5
- def screenshot
6
- forward_url = login
7
- # Extract the key-value pairs from the forward URL
8
- # index?ST1=ABC,ST2=DEF
9
- tokens = forward_url.split("?").last.split(",").inject({}) { |acc,kv| k,v = kv.split("="); acc[k]=v; acc }
10
- timestamp_ms = (Time.now.to_f * 1000).to_i
11
- path = "data?get=consolepreview[manual%20#{timestamp_ms}]"
12
- res = get(path: path, headers: tokens)
13
- raise "Invalid login." unless res.code.between?(200, 299)
14
- sleep 2
15
- path = "capconsole/scapture0.png?#{timestamp_ms}"
16
- res = get(path: path, headers: tokens)
17
- raise "Invalid login." unless res.code.between?(200, 299)
18
- filename = "#{timestamp_ms}.png"
19
- File.write(filename, res.body)
20
- logout
21
- filename
22
- end
23
- end
24
- end