idrac 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -1
- data/bin/idrac +9 -2
- data/idrac.gemspec +1 -1
- data/lib/idrac/boot.rb +2 -18
- data/lib/idrac/client.rb +18 -6
- data/lib/idrac/jobs.rb +49 -1
- data/lib/idrac/power.rb +21 -11
- data/lib/idrac/system_config.rb +28 -3
- data/lib/idrac/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17491587256894c2ff168665dca777903a2d6b7d7582f745c4c3e81f697889cd
|
|
4
|
+
data.tar.gz: 432ecbfd6df649a69644254dc942e253a8d917f6ce2cd07c30f7f328c3ffee75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28cc04b7ec2f246ed29d95ba50481f52dff7893e69fd94dc6f82db81e0d7b988e6d0e21589ae09a75e99c51621f2a6650a1aa0ada2c74c8aa4ff6f2726cadf98
|
|
7
|
+
data.tar.gz: 6981e38fe25c803372a0c09aa3616db6ce7d0670415fbec9f73e143c25e44fff25e24ea650dfe5ebfcc821b14c6d17dd778664e43d11b5fbfd8c6458cacae374
|
data/README.md
CHANGED
|
@@ -167,9 +167,14 @@ client.clear_jobs!
|
|
|
167
167
|
# Force clear job queue (use with caution)
|
|
168
168
|
client.force_clear_jobs!
|
|
169
169
|
|
|
170
|
-
# Wait for a specific job to complete
|
|
170
|
+
# Wait for a specific job to complete (raises IDRAC::Error if the job failed)
|
|
171
171
|
job_data = client.wait_for_job("JID_12345678")
|
|
172
172
|
|
|
173
|
+
# Wait for a job and get its outcome back instead of an exception
|
|
174
|
+
result = client.wait_for_job_completion("JID_12345678")
|
|
175
|
+
# => { status: :success, job_id: "JID_12345678", job_state: "Completed",
|
|
176
|
+
# message: "...", messages: ["..."], job: { ... } }
|
|
177
|
+
|
|
173
178
|
# Lifecycle Controller operations
|
|
174
179
|
# Check if Lifecycle Controller is enabled
|
|
175
180
|
status = client.get_idrac_lifecycle_status
|
|
@@ -291,6 +296,30 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
291
296
|
|
|
292
297
|
## Changelog
|
|
293
298
|
|
|
299
|
+
### Version 0.10.3
|
|
300
|
+
- **Fixed: SCP imports reported as failures when they had succeeded**: `set_system_configuration_profile` waited on the *task* that submitted the import. iDRAC answers the import with a TaskService URI whose id is the id of the job it queued, and that task sits at `TaskState "Pending"` / `"New job."` while the job does the real work. The transient task state was returned as `{status: :failed, task_state: "Pending", messages: ["New job."]}`, so callers aborted on imports that went on to complete.
|
|
301
|
+
- The import now waits for the **job** to reach a terminal state and returns the **job's** outcome
|
|
302
|
+
- Return value: `{ status: :success | :failed | :timeout, job_id:, job_state:, message:, messages:, job: }`, plus `error:` for anything other than `:success` (`:status` is unchanged; `:job_id` is new, so callers no longer have to diff the job list to find the job)
|
|
303
|
+
- `Failed`, `CompletedWithErrors`, and `RebootFailed` surface the job's own `Message`
|
|
304
|
+
- A location header that points straight at a job still uses the job path
|
|
305
|
+
- New `timeout:` option (default 600s)
|
|
306
|
+
- **New `wait_for_job_completion(job_id, timeout:, interval:)`**: waits for a job and returns its outcome as a hash rather than raising, keeps polling while the host is briefly unreachable (an SCP import can bounce it), and always reports the job id
|
|
307
|
+
- **Fixed `wait_for_task` treating a queued task as failed**: it broke out of its poll loop on any state other than `Running`, so `New`/`Pending`/`Starting` were reported as failures. It now waits through every non-terminal state, accepts a full location header, and takes a `timeout:` (default 600s)
|
|
308
|
+
- **Removed stray `debugger` calls** from `wait_for_task` and `import_system_configuration`
|
|
309
|
+
|
|
310
|
+
### Version 0.10.2
|
|
311
|
+
- **Fixed: firmware updates matched the wrong Dell Update Package**: `check_updates` paired an installed component with a catalog DUP by display name. Names are ambiguous — every NIC package reads "Ethernet" — so it selected packages that do not flash the installed device. iDRAC rejected those on apply (RED097 / "not compatible") and the version column compared unrelated components.
|
|
312
|
+
- Matching is now done on the Dell **componentID**, the key that actually identifies the device
|
|
313
|
+
- `find_updates_for_system` records `component_ids:` for each DUP, read from `SupportedDevices/Device/@componentID`
|
|
314
|
+
- `get_system_inventory` records `component_id:` for each installed component, from the Dell OEM block when present and otherwise parsed out of the inventory `Id` (`Installed-<componentID>-<version>`), which is all iDRAC8 exposes
|
|
315
|
+
- New `FirmwareCatalog#updates_for_component(catalog_updates, fw)` does the matching, and falls back to the old name heuristic only when the inventory reports no componentID (or reports `"0"`)
|
|
316
|
+
- New `FirmwareCatalog#version_key(version)` gives a sortable key, so when the catalog lists several revisions for one component the newest is chosen
|
|
317
|
+
- **Fixed: every component reported as out of date**: the catalog version came from `dellVersion`, which is the package revision label (`A17`), not a version. Comparing that against an installed numeric version (`25.5.9.0001`) flagged everything. `vendorVersion` is now preferred.
|
|
318
|
+
|
|
319
|
+
### Versions 0.10.0-0.10.1
|
|
320
|
+
- Use the Dell OEM `Install` action on iDRAC8 instead of `SimpleUpdate`
|
|
321
|
+
- Fixed a false-positive virtual-media "inserted" state on iDRAC8 (benign VRM0009 500 on eject is now a no-op)
|
|
322
|
+
|
|
294
323
|
### Version 0.9.1
|
|
295
324
|
- **Code Simplification**: Removed redundant `handle_response` calls throughout the codebase
|
|
296
325
|
- `authenticated_request` now automatically handles error responses (status >= 400)
|
data/bin/idrac
CHANGED
|
@@ -1663,12 +1663,19 @@ module IDRAC
|
|
|
1663
1663
|
# TSR (Technical Support Report) commands
|
|
1664
1664
|
desc "tsr_collect", "Generate and download TSR/SupportAssist collection"
|
|
1665
1665
|
method_option :output, type: :string, desc: "Output filename"
|
|
1666
|
+
method_option :timeout, type: :numeric, default: 600, desc: "Seconds to wait for collection to complete"
|
|
1667
|
+
method_option :data_types, type: :string, desc: "Comma-separated log types to collect (0=HWData, 1=OSAppData, 2=TTYLogs, 3=DebugLogs; default: HWData,OSAppData)"
|
|
1666
1668
|
def tsr_collect
|
|
1667
1669
|
with_idrac_client do |client|
|
|
1668
1670
|
puts "Generating and downloading TSR logs...".yellow
|
|
1669
1671
|
puts "This may take several minutes...".yellow
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
+
|
|
1673
|
+
data_selector = options[:data_types]&.split(',')
|
|
1674
|
+
result = client.generate_and_download_tsr(
|
|
1675
|
+
output_file: options[:output],
|
|
1676
|
+
data_selector_values: data_selector,
|
|
1677
|
+
wait_timeout: options[:timeout]
|
|
1678
|
+
)
|
|
1672
1679
|
|
|
1673
1680
|
if result
|
|
1674
1681
|
puts "TSR logs saved to: #{result}".green
|
data/idrac.gemspec
CHANGED
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.require_paths = ["lib"]
|
|
35
35
|
|
|
36
36
|
# Dependencies - Using semantic versioning recommendations
|
|
37
|
-
spec.add_dependency "httparty", "~> 0.
|
|
37
|
+
spec.add_dependency "httparty", "~> 0.24", ">= 0.24.0"
|
|
38
38
|
spec.add_dependency "nokogiri", "~> 1.15", ">= 1.15.0"
|
|
39
39
|
spec.add_dependency "faraday", "~> 2.7", ">= 2.7.0"
|
|
40
40
|
spec.add_dependency "faraday-multipart", "~> 1.0", ">= 1.0.0"
|
data/lib/idrac/boot.rb
CHANGED
|
@@ -323,22 +323,7 @@ module IDRAC
|
|
|
323
323
|
raise Error, "Failed to get BIOS information. Status code: #{response.status}"
|
|
324
324
|
end
|
|
325
325
|
end
|
|
326
|
-
=begin
|
|
327
|
-
# Servers can boot in BIOS mode or in UEFI (modern, extensible BIOS replacement) mode.
|
|
328
|
-
# We use UEFI mode.
|
|
329
|
-
# self.get(path: "Systems/System.Embedded.1/Bios/Settings?$select=BootMode")
|
|
330
|
-
res = self.get(path: "Systems/System.Embedded.1/Bios")
|
|
331
|
-
if res["body"]["Attributes"]["BootMode"] == "Uefi"
|
|
332
|
-
return { status: :success }
|
|
333
|
-
else
|
|
334
|
-
res = self.set_system_configuration_profile(scp_boot_mode_uefi, reboot: true)
|
|
335
|
-
# Then must power cycle the server
|
|
336
|
-
self.power_on!(wait: true)
|
|
337
|
-
self.power_off!(wait: true)
|
|
338
|
-
return res
|
|
339
|
-
end
|
|
340
326
|
|
|
341
|
-
=end
|
|
342
327
|
def scp_boot_mode_uefi(idrac_license_version: 9)
|
|
343
328
|
opts = { "BootMode" => 'Uefi' }
|
|
344
329
|
# If we're iDRAC 9, we need enable a placeholder, otherwise we can't order the
|
|
@@ -675,9 +660,8 @@ module IDRAC
|
|
|
675
660
|
body: params.to_json
|
|
676
661
|
)
|
|
677
662
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return task
|
|
663
|
+
# Same operation as set_system_configuration_profile: wait on the import JOB.
|
|
664
|
+
return wait_for_scp_import(response.headers["location"])
|
|
681
665
|
end
|
|
682
666
|
end
|
|
683
667
|
end
|
data/lib/idrac/client.rb
CHANGED
|
@@ -392,10 +392,18 @@ module IDRAC
|
|
|
392
392
|
raise e
|
|
393
393
|
end
|
|
394
394
|
end
|
|
395
|
-
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Task states that mean the task has not produced a result yet. A task that has only
|
|
398
|
+
# been queued reports "Pending"/"New" ("New job."), which is not an outcome -- so we
|
|
399
|
+
# keep waiting instead of reporting that transient state as a failure.
|
|
400
|
+
TASK_INCOMPLETE_STATES = %w[New Starting Pending Running Stopping Cancelling].freeze
|
|
396
401
|
|
|
397
|
-
|
|
402
|
+
# Wait for a task to complete
|
|
403
|
+
def wait_for_task(task_id, timeout: 600)
|
|
404
|
+
task_id = task_id.to_s.split("/").last # tolerate being handed a full location header
|
|
398
405
|
task = nil
|
|
406
|
+
deadline = Time.now + timeout
|
|
399
407
|
|
|
400
408
|
begin
|
|
401
409
|
loop do
|
|
@@ -406,9 +414,7 @@ module IDRAC
|
|
|
406
414
|
when 200..299
|
|
407
415
|
task = JSON.parse(task_response.body)
|
|
408
416
|
|
|
409
|
-
|
|
410
|
-
break
|
|
411
|
-
end
|
|
417
|
+
break unless TASK_INCOMPLETE_STATES.include?(task["TaskState"])
|
|
412
418
|
|
|
413
419
|
# Extract percentage complete if available
|
|
414
420
|
percent_complete = nil
|
|
@@ -418,6 +424,12 @@ module IDRAC
|
|
|
418
424
|
end
|
|
419
425
|
|
|
420
426
|
debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
|
|
427
|
+
|
|
428
|
+
if Time.now >= deadline
|
|
429
|
+
return { status: :timeout, task_state: task["TaskState"], task_status: task["TaskStatus"],
|
|
430
|
+
error: "Timed out after #{timeout}s waiting for task #{task_id} (state: #{task["TaskState"]})" }
|
|
431
|
+
end
|
|
432
|
+
|
|
421
433
|
sleep 5
|
|
422
434
|
else
|
|
423
435
|
return {
|
|
@@ -451,7 +463,7 @@ module IDRAC
|
|
|
451
463
|
}
|
|
452
464
|
end
|
|
453
465
|
rescue => e
|
|
454
|
-
|
|
466
|
+
debug "Exception monitoring task #{task_id}: #{e.message}", 1, :red
|
|
455
467
|
return { status: :error, error: "Exception monitoring task: #{e.message}" }
|
|
456
468
|
end
|
|
457
469
|
end
|
data/lib/idrac/jobs.rb
CHANGED
|
@@ -185,7 +185,55 @@ module IDRAC
|
|
|
185
185
|
|
|
186
186
|
raise Error, "Timeout waiting for job to complete"
|
|
187
187
|
end
|
|
188
|
-
|
|
188
|
+
|
|
189
|
+
# Job states that mean the job is finished, one way or another.
|
|
190
|
+
JOB_TERMINAL_STATES = %w[Completed CompletedWithErrors Failed RebootFailed].freeze
|
|
191
|
+
|
|
192
|
+
# Wait for an iDRAC job to reach a terminal state and report on the JOB itself.
|
|
193
|
+
#
|
|
194
|
+
# Unlike wait_for_job this does not raise for a job that simply failed -- it returns
|
|
195
|
+
# the outcome so the caller can log or re-check it. The hash always carries :job_id.
|
|
196
|
+
#
|
|
197
|
+
# { status: :success | :failed | :timeout, job_id:, job_state:,
|
|
198
|
+
# message:, messages: [message], job: <raw job data>,
|
|
199
|
+
# error: <the job's own message, on anything but :success> }
|
|
200
|
+
def wait_for_job_completion(job_id, timeout: 600, interval: 10)
|
|
201
|
+
job_id = job_id.to_s.split("/").last
|
|
202
|
+
deadline = Time.now + timeout
|
|
203
|
+
state = nil
|
|
204
|
+
last_error = nil
|
|
205
|
+
|
|
206
|
+
while Time.now < deadline
|
|
207
|
+
begin
|
|
208
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{job_id}")
|
|
209
|
+
job = response.status == 200 ? JSON.parse(response.body) : nil
|
|
210
|
+
state = job ? job["JobState"] : nil
|
|
211
|
+
|
|
212
|
+
if JOB_TERMINAL_STATES.include?(state)
|
|
213
|
+
message = job["Message"] || Array(job["Messages"]).map { |m| m["Message"] }.compact.first
|
|
214
|
+
success = state == "Completed"
|
|
215
|
+
debug "Job #{job_id} #{state}: #{message}", 1, success ? :green : :red
|
|
216
|
+
result = { status: success ? :success : :failed, job_id: job_id, job_state: state,
|
|
217
|
+
message: message, messages: [message].compact, job: job }
|
|
218
|
+
result[:error] = message || "Job #{job_id} finished with state #{state}" unless success
|
|
219
|
+
return result
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
last_error = "HTTP #{response.status}" unless job
|
|
223
|
+
debug "Job #{job_id} state: #{state || last_error}. Waiting...", 1, :yellow
|
|
224
|
+
rescue StandardError => e
|
|
225
|
+
# An SCP import can bounce the host, so a job can be briefly unreadable. Keep polling.
|
|
226
|
+
last_error = e.message
|
|
227
|
+
debug "Job #{job_id} not readable (#{e.message}). Waiting...", 1, :yellow
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
sleep interval
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
{ status: :timeout, job_id: job_id, job_state: state,
|
|
234
|
+
error: "Timed out after #{timeout}s waiting for job #{job_id} (last state: #{state || last_error || 'unknown'})" }
|
|
235
|
+
end
|
|
236
|
+
|
|
189
237
|
# Get system tasks
|
|
190
238
|
def tasks
|
|
191
239
|
response = authenticated_request(:get, '/redfish/v1/TaskService/Tasks')
|
data/lib/idrac/power.rb
CHANGED
|
@@ -58,7 +58,15 @@ module IDRAC
|
|
|
58
58
|
return true
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Reset types that restart the host rather than leave it down. A restart never settles on
|
|
62
|
+
# "Off", so it must never be waited on — or escalated to — as if it were a shutdown.
|
|
63
|
+
RESTART_TYPES = %w[ForceRestart GracefulRestart PowerCycle].freeze
|
|
64
|
+
|
|
61
65
|
def power_off(wait: true, kind: "ForceOff")
|
|
66
|
+
# A restart is not a shutdown. Sent down the power-off path it fails the wait for "Off" and
|
|
67
|
+
# escalates to ForceOff, which leaves the host powered down. Route it to the restart instead.
|
|
68
|
+
return reboot(kind: kind, wait: wait) if RESTART_TYPES.include?(kind)
|
|
69
|
+
|
|
62
70
|
ensure_authenticated!
|
|
63
71
|
|
|
64
72
|
puts "Powering off server...".light_cyan
|
|
@@ -93,37 +101,39 @@ module IDRAC
|
|
|
93
101
|
return true
|
|
94
102
|
end
|
|
95
103
|
|
|
96
|
-
def reboot
|
|
104
|
+
def reboot(kind: "ForceRestart", wait: false)
|
|
97
105
|
ensure_authenticated!
|
|
98
106
|
|
|
99
|
-
puts "Rebooting server...".light_cyan
|
|
107
|
+
puts "Rebooting server (#{kind})...".light_cyan
|
|
100
108
|
|
|
101
109
|
# Check current power state first
|
|
102
110
|
current_state = get_power_state rescue "Unknown"
|
|
103
111
|
if current_state == "Off"
|
|
104
112
|
puts "Server is currently off, powering on instead of rebooting".yellow
|
|
105
|
-
return power_on
|
|
113
|
+
return power_on(wait: wait)
|
|
106
114
|
end
|
|
107
115
|
|
|
108
|
-
# Send reboot command (Reset with ResetType=ForceRestart)
|
|
109
116
|
path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
response = authenticated_request(:post, path, body: payload.to_json)
|
|
117
|
+
response = authenticated_request(:post, path, body: { "ResetType" => kind }.to_json)
|
|
113
118
|
|
|
114
119
|
if response.status >= 200 && response.status < 300
|
|
115
120
|
puts "Server reboot command sent successfully".green
|
|
116
|
-
|
|
117
|
-
elsif response.status == 409
|
|
121
|
+
elsif response.status == 409 && kind != "GracefulRestart"
|
|
118
122
|
error_data = JSON.parse(response.body) rescue nil
|
|
119
123
|
puts "Received conflict (409) error from iDRAC: #{error_data.inspect}"
|
|
120
124
|
# Try gracefulRestart as an alternative
|
|
121
125
|
puts "Trying GracefulRestart instead...".yellow
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
response = authenticated_request(:post, path, body: { "ResetType" => "GracefulRestart" }.to_json)
|
|
127
|
+
raise Error, "Failed to reboot server. Status code: #{response.status}" unless response.status.between?(200, 299)
|
|
124
128
|
else
|
|
125
129
|
raise Error, "Failed to reboot server. Status code: #{response.status}"
|
|
126
130
|
end
|
|
131
|
+
|
|
132
|
+
# "On" is the only terminal state a restart reaches. Never wait for "Off" here: the host may
|
|
133
|
+
# never report it, and treating that as failure is what turned a reboot into a shutdown.
|
|
134
|
+
wait_for_power_state(target_state: "On", tries: 12) if wait
|
|
135
|
+
|
|
136
|
+
return true
|
|
127
137
|
end
|
|
128
138
|
|
|
129
139
|
def get_power_state
|
data/lib/idrac/system_config.rb
CHANGED
|
@@ -84,8 +84,13 @@ module IDRAC
|
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
# Apply a system configuration profile to the iDRAC
|
|
88
|
-
|
|
87
|
+
# Apply a system configuration profile to the iDRAC.
|
|
88
|
+
#
|
|
89
|
+
# Waits for the import JOB to finish and returns the JOB's outcome:
|
|
90
|
+
#
|
|
91
|
+
# { status: :success | :failed | :timeout, job_id:, job_state:, message:,
|
|
92
|
+
# messages: [message], job: <raw job data>, error: <message unless :success> }
|
|
93
|
+
def set_system_configuration_profile(scp, target: "ALL", reboot: false, timeout: 600, retry_count: 0)
|
|
89
94
|
# Ensure scp has the proper structure with SystemConfiguration wrapper
|
|
90
95
|
scp_to_apply = if scp.is_a?(Hash) && scp["SystemConfiguration"]
|
|
91
96
|
scp
|
|
@@ -135,7 +140,27 @@ module IDRAC
|
|
|
135
140
|
return { status: :failed, error: error_message }
|
|
136
141
|
end
|
|
137
142
|
|
|
138
|
-
return
|
|
143
|
+
return wait_for_scp_import(response.headers["location"], timeout: timeout)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Wait for an SCP import submitted at +location+ and report the JOB's outcome.
|
|
147
|
+
#
|
|
148
|
+
# iDRAC answers the import with a TaskService URI whose id is the id of the job it
|
|
149
|
+
# queued (/redfish/v1/TaskService/Tasks/JID_...). That task is only the submission:
|
|
150
|
+
# it sits at TaskState "Pending" / "New job." while the job does the real work, so
|
|
151
|
+
# waiting on the task reports a queued job as a failure. The job is the operation,
|
|
152
|
+
# so the job is what we wait on -- and the job id goes back to the caller.
|
|
153
|
+
def wait_for_scp_import(location, timeout: 600)
|
|
154
|
+
return { status: :failed, error: "SCP import returned no location header" } if location.nil? || location.to_s.empty?
|
|
155
|
+
|
|
156
|
+
id = location.to_s.split("/").last
|
|
157
|
+
# Dell job ids (JID_/RID_) name the job whether the header points at the job itself
|
|
158
|
+
# or at the task that queued it. Anything else is a real task: handle it as before.
|
|
159
|
+
if location.to_s.include?("/Jobs/") || id.start_with?("JID_", "RID_")
|
|
160
|
+
wait_for_job_completion(id, timeout: timeout)
|
|
161
|
+
else
|
|
162
|
+
handle_location(location)
|
|
163
|
+
end
|
|
139
164
|
end
|
|
140
165
|
|
|
141
166
|
# This puts the SCP into a format that can be used by reasonable Ruby code.
|
data/lib/idrac/version.rb
CHANGED
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.10.
|
|
4
|
+
version: 0.10.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Siegel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: httparty
|
|
@@ -16,20 +16,20 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.
|
|
19
|
+
version: '0.24'
|
|
20
20
|
- - ">="
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: 0.
|
|
22
|
+
version: 0.24.0
|
|
23
23
|
type: :runtime
|
|
24
24
|
prerelease: false
|
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
26
|
requirements:
|
|
27
27
|
- - "~>"
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: '0.
|
|
29
|
+
version: '0.24'
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.24.0
|
|
33
33
|
- !ruby/object:Gem::Dependency
|
|
34
34
|
name: nokogiri
|
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|