eaton 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +9 -7
- data/TODO.md +81 -0
- data/lib/eaton/cli.rb +5 -8
- data/lib/eaton/client.rb +73 -1
- data/lib/eaton/network.rb +37 -0
- data/lib/eaton/power.rb +6 -6
- data/lib/eaton/version.rb +1 -1
- data/lib/eaton.rb +2 -1
- metadata +6 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb4213ff1de1459504fd9aef3ff717e711d4926dcc482af271a5ced054c4dd72
|
|
4
|
+
data.tar.gz: 5cb7bf81a8fc1cd3cba0d27407f352731bb49a07a22e210b87ddcd6fffe5eb9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d64daf3ed46a838ee8e1b0b94501122af4b4124c1c9fe934fab670d504268ccf7809470690d69ce102d4ccfa5cde454e03b9f4585444d54b6ffbe1379d70a18
|
|
7
|
+
data.tar.gz: 8bb820f4d75d9149196d12da53af22e8b75dbd2521894c9b8cfa6d64d09976f0a72c1a35818553ce5a66203b9aa91d86596a486b915c2df9cd60de28c900a72a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-09-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Client#put` for settings writes; the client could only read, POST and DELETE
|
|
12
|
+
- `Eaton::Network`, mixed into `Client`:
|
|
13
|
+
- `#ipv4` reports the address, mask, gateway and mode eth0 is running on
|
|
14
|
+
- `#set_ipv4(address:, subnet_mask:, gateway:)` switches eth0 to a static
|
|
15
|
+
address. All three are required and a blank one raises `ArgumentError`:
|
|
16
|
+
the PDU applies them together, and a wrong mask can leave it unreachable.
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- Enhanced authentication error handling for expired credentials
|
|
20
|
+
- Added detailed password policy requirements in error messages
|
|
21
|
+
- Improved user guidance for first-time login and password expiration scenarios
|
|
22
|
+
- Error messages now include direct instructions for changing password via web interface
|
|
23
|
+
|
|
8
24
|
## [0.1.0] - 2025-01-20
|
|
9
25
|
|
|
10
26
|
### Added
|
data/README.md
CHANGED
|
@@ -176,29 +176,31 @@ client = Eaton::Client.new(
|
|
|
176
176
|
verify_ssl: true
|
|
177
177
|
)
|
|
178
178
|
|
|
179
|
-
# Extend with power monitoring
|
|
180
|
-
client.extend(Eaton::Power)
|
|
181
|
-
|
|
182
179
|
# Get PDU info
|
|
183
|
-
info = client.
|
|
180
|
+
info = client.info
|
|
184
181
|
puts "#{info[:model]} - #{info[:serial_number]}"
|
|
185
182
|
|
|
186
183
|
# Get overall power
|
|
187
|
-
power = client.
|
|
184
|
+
power = client.power
|
|
188
185
|
puts "Current draw: #{power} watts"
|
|
189
186
|
|
|
190
187
|
# Get active outlets
|
|
191
|
-
outlets = client.
|
|
188
|
+
outlets = client.outlets
|
|
192
189
|
outlets.select { |o| o[:watts] > 0 }.each do |outlet|
|
|
193
190
|
puts "#{outlet[:name]}: #{outlet[:watts]}W"
|
|
194
191
|
end
|
|
195
192
|
|
|
196
193
|
# Get branch distribution
|
|
197
|
-
branches = client.
|
|
194
|
+
branches = client.branches
|
|
198
195
|
branches.each do |branch|
|
|
199
196
|
puts "#{branch[:name]}: #{branch[:current]}A @ #{branch[:voltage]}V"
|
|
200
197
|
end
|
|
201
198
|
|
|
199
|
+
# Get detailed metrics
|
|
200
|
+
detailed = client.detailed
|
|
201
|
+
puts "Power Factor: #{detailed[:overall][:power_factor]}"
|
|
202
|
+
puts "Frequency: #{detailed[:overall][:frequency]} Hz"
|
|
203
|
+
|
|
202
204
|
# Clean up
|
|
203
205
|
client.logout
|
|
204
206
|
```
|
data/TODO.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Future Enhancements
|
|
4
|
+
|
|
5
|
+
### Programmatic Password Change (Issue #1)
|
|
6
|
+
|
|
7
|
+
**Priority**: Medium
|
|
8
|
+
**Context**: First-time login and password expiration handling
|
|
9
|
+
|
|
10
|
+
**Problem**:
|
|
11
|
+
Eaton PDUs enforce password expiration policies. When credentials expire, users currently must:
|
|
12
|
+
1. Navigate to the PDU web interface
|
|
13
|
+
2. Manually change the password
|
|
14
|
+
3. Update credentials in their application
|
|
15
|
+
|
|
16
|
+
This creates friction, especially for:
|
|
17
|
+
- First-time setup (initial default password change)
|
|
18
|
+
- Automated systems that need to handle password rotation
|
|
19
|
+
- Multiple PDU management scenarios
|
|
20
|
+
|
|
21
|
+
**Proposed Solution**:
|
|
22
|
+
Add programmatic password change capability to the client:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Ruby API
|
|
26
|
+
client = Eaton::Client.new(
|
|
27
|
+
host: 'pdu.example.com',
|
|
28
|
+
username: 'admin',
|
|
29
|
+
password: 'current_password'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
client.change_password(new_password: 'NewSecurePass123!')
|
|
33
|
+
|
|
34
|
+
# CLI
|
|
35
|
+
eaton change-password \
|
|
36
|
+
--host pdu.example.com \
|
|
37
|
+
--username admin \
|
|
38
|
+
--current-password old_pass \
|
|
39
|
+
--new-password new_pass
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Implementation Requirements**:
|
|
43
|
+
1. Research Eaton PDU API endpoint for password changes
|
|
44
|
+
2. Implement `change_password` method in `Eaton::Client`
|
|
45
|
+
3. Add password policy validation before attempting change
|
|
46
|
+
4. Create CLI command `eaton change-password`
|
|
47
|
+
5. Handle special case: changing expired credentials
|
|
48
|
+
6. Add comprehensive error handling
|
|
49
|
+
7. Update documentation with password management guide
|
|
50
|
+
8. Add tests for password change functionality
|
|
51
|
+
|
|
52
|
+
**API Research Needed**:
|
|
53
|
+
- Identify correct REST endpoint (likely `/users/{id}` or `/accounts/{id}`)
|
|
54
|
+
- Determine if special authentication is needed for expired credentials
|
|
55
|
+
- Understand session requirements during password change
|
|
56
|
+
- Check if password history/reuse policies are enforced
|
|
57
|
+
|
|
58
|
+
**Security Considerations**:
|
|
59
|
+
- Never log passwords
|
|
60
|
+
- Clear old password from memory after use
|
|
61
|
+
- Validate new password against policy before submitting
|
|
62
|
+
- Handle authentication state correctly during transition
|
|
63
|
+
- Consider secure input methods (no command-line password echo)
|
|
64
|
+
|
|
65
|
+
**Testing Approach**:
|
|
66
|
+
- Mock API responses for password change
|
|
67
|
+
- Test policy validation
|
|
68
|
+
- Test error scenarios (wrong current password, policy violations)
|
|
69
|
+
- Integration test with real PDU (manual, not CI)
|
|
70
|
+
|
|
71
|
+
**Documentation Updates**:
|
|
72
|
+
- Add password management section to README
|
|
73
|
+
- Include examples for both API and CLI
|
|
74
|
+
- Document password policy requirements
|
|
75
|
+
- Add troubleshooting guide for common scenarios
|
|
76
|
+
|
|
77
|
+
**Related Files**:
|
|
78
|
+
- `lib/eaton/client.rb` - Add change_password method
|
|
79
|
+
- `lib/eaton/cli.rb` - Add CLI command
|
|
80
|
+
- `spec/*` - Add test coverage
|
|
81
|
+
- `README.md` - Update documentation
|
data/lib/eaton/cli.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Eaton
|
|
|
16
16
|
desc "power", "Get overall power consumption in watts"
|
|
17
17
|
def power
|
|
18
18
|
with_client do |client|
|
|
19
|
-
power = client.
|
|
19
|
+
power = client.power
|
|
20
20
|
output_result("Overall Power", { watts: power })
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -24,7 +24,7 @@ module Eaton
|
|
|
24
24
|
desc "outlets", "Get per-outlet power consumption"
|
|
25
25
|
def outlets
|
|
26
26
|
with_client do |client|
|
|
27
|
-
outlets = client.
|
|
27
|
+
outlets = client.outlets
|
|
28
28
|
# Filter out zero-power outlets in text mode
|
|
29
29
|
if options[:format] == "text"
|
|
30
30
|
outlets = outlets.select { |o| o[:watts] && o[:watts] > 0 }
|
|
@@ -36,7 +36,7 @@ module Eaton
|
|
|
36
36
|
desc "detailed", "Get detailed power information"
|
|
37
37
|
def detailed
|
|
38
38
|
with_client do |client|
|
|
39
|
-
info = client.
|
|
39
|
+
info = client.detailed
|
|
40
40
|
# Filter outlets in text mode
|
|
41
41
|
if options[:format] == "text" && info[:outlets]
|
|
42
42
|
info[:outlets] = info[:outlets].select { |o| o[:watts] && o[:watts] > 0 }
|
|
@@ -48,7 +48,7 @@ module Eaton
|
|
|
48
48
|
desc "branches", "Get power consumption per branch"
|
|
49
49
|
def branches
|
|
50
50
|
with_client do |client|
|
|
51
|
-
branches = client.
|
|
51
|
+
branches = client.branches
|
|
52
52
|
# Filter out zero-current branches in text mode
|
|
53
53
|
if options[:format] == "text"
|
|
54
54
|
branches = branches.select { |b| b[:current] && b[:current] > 0 }
|
|
@@ -60,7 +60,7 @@ module Eaton
|
|
|
60
60
|
desc "info", "Display PDU device information"
|
|
61
61
|
def info
|
|
62
62
|
with_client do |client|
|
|
63
|
-
info = client.
|
|
63
|
+
info = client.info
|
|
64
64
|
output_result("PDU Device Information", info)
|
|
65
65
|
end
|
|
66
66
|
end
|
|
@@ -88,9 +88,6 @@ module Eaton
|
|
|
88
88
|
host_header: options[:host_header]
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
# Mix in the Power module to add power monitoring methods
|
|
92
|
-
client.extend(Power)
|
|
93
|
-
|
|
94
91
|
yield client
|
|
95
92
|
rescue Client::AuthenticationError => e
|
|
96
93
|
error("Authentication failed: #{e.message}")
|
data/lib/eaton/client.rb
CHANGED
|
@@ -10,6 +10,9 @@ module Eaton
|
|
|
10
10
|
class AuthenticationError < StandardError; end
|
|
11
11
|
class APIError < StandardError; end
|
|
12
12
|
|
|
13
|
+
include Power
|
|
14
|
+
include Network
|
|
15
|
+
|
|
13
16
|
attr_reader :host, :username, :base_url
|
|
14
17
|
|
|
15
18
|
def initialize(host:, username:, password:, port: 443, verify_ssl: false, host_header: nil)
|
|
@@ -37,7 +40,7 @@ module Eaton
|
|
|
37
40
|
@session = data["session"]
|
|
38
41
|
@token
|
|
39
42
|
else
|
|
40
|
-
|
|
43
|
+
handle_auth_error(response)
|
|
41
44
|
end
|
|
42
45
|
rescue JSON::ParserError => e
|
|
43
46
|
raise AuthenticationError, "Invalid response from server: #{e.message}"
|
|
@@ -66,6 +69,16 @@ module Eaton
|
|
|
66
69
|
handle_response(execute_request(request))
|
|
67
70
|
end
|
|
68
71
|
|
|
72
|
+
def put(path, data = {})
|
|
73
|
+
authenticate! unless authenticated?
|
|
74
|
+
|
|
75
|
+
request = Net::HTTP::Put.new("#{@base_path}#{path}")
|
|
76
|
+
add_auth_headers(request)
|
|
77
|
+
request.body = data.to_json
|
|
78
|
+
|
|
79
|
+
handle_response(execute_request(request))
|
|
80
|
+
end
|
|
81
|
+
|
|
69
82
|
def logout
|
|
70
83
|
return unless authenticated?
|
|
71
84
|
|
|
@@ -81,6 +94,65 @@ module Eaton
|
|
|
81
94
|
|
|
82
95
|
private
|
|
83
96
|
|
|
97
|
+
def handle_auth_error(response)
|
|
98
|
+
begin
|
|
99
|
+
error_data = JSON.parse(response.body)
|
|
100
|
+
|
|
101
|
+
# Check for expired credentials error
|
|
102
|
+
if error_data["code"] == 112 && error_data["description"] =~ /expired/i
|
|
103
|
+
policy = parse_password_policy(error_data["args"])
|
|
104
|
+
message = build_expired_credentials_message(policy)
|
|
105
|
+
raise AuthenticationError, message
|
|
106
|
+
end
|
|
107
|
+
rescue JSON::ParserError
|
|
108
|
+
# Fall through to generic error
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Generic authentication error
|
|
112
|
+
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_password_policy(args)
|
|
116
|
+
return nil unless args && args.is_a?(Array) && args.length >= 7
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
min_length: args[0],
|
|
120
|
+
max_length: args[1],
|
|
121
|
+
require_uppercase: args[2] == 1,
|
|
122
|
+
require_lowercase: args[3] == 1,
|
|
123
|
+
require_numbers: args[4] == 1,
|
|
124
|
+
require_special: args[5] == 1,
|
|
125
|
+
special_chars: args[6]
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_expired_credentials_message(policy)
|
|
130
|
+
message = "Credentials are expired. Please change the password via the PDU web interface.\n\n"
|
|
131
|
+
|
|
132
|
+
if policy
|
|
133
|
+
message += "Password requirements:\n"
|
|
134
|
+
message += " - Length: #{policy[:min_length]}-#{policy[:max_length]} characters\n"
|
|
135
|
+
|
|
136
|
+
requirements = []
|
|
137
|
+
requirements << "uppercase letters" if policy[:require_uppercase]
|
|
138
|
+
requirements << "lowercase letters" if policy[:require_lowercase]
|
|
139
|
+
requirements << "numbers" if policy[:require_numbers]
|
|
140
|
+
requirements << "special characters" if policy[:require_special]
|
|
141
|
+
|
|
142
|
+
message += " - Must include: #{requirements.join(', ')}\n" if requirements.any?
|
|
143
|
+
message += " - Allowed special characters: #{policy[:special_chars]}\n" if policy[:special_chars]
|
|
144
|
+
message += "\n"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
message += "To change password:\n"
|
|
148
|
+
message += " 1. Navigate to https://#{@host}#{@port == 443 ? '' : ":#{@port}"}\n"
|
|
149
|
+
message += " 2. Log in with current credentials\n"
|
|
150
|
+
message += " 3. Follow prompts to change password\n"
|
|
151
|
+
message += "\nNote: First-time login may require password change before API access is granted."
|
|
152
|
+
|
|
153
|
+
message
|
|
154
|
+
end
|
|
155
|
+
|
|
84
156
|
def http_connection
|
|
85
157
|
@http_connection ||= begin
|
|
86
158
|
http = Net::HTTP.new(@host, @port)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eaton
|
|
4
|
+
# eth0 addressing on the PDU's network management card.
|
|
5
|
+
module Network
|
|
6
|
+
ETH0_PATH = "/managers/1/networkService/networkInterfaces/eth0"
|
|
7
|
+
|
|
8
|
+
# Current eth0 addressing. :mode is "dhcp client" or "manual"; the address,
|
|
9
|
+
# subnet_mask and gateway are what the interface is running on right now,
|
|
10
|
+
# which is what DHCP handed it when the mode is dhcp.
|
|
11
|
+
def ipv4
|
|
12
|
+
data = get(ETH0_PATH)
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
mode: data.dig("ipv4", "settings", "mode"),
|
|
16
|
+
address: data.dig("ipv4", "status", "address"),
|
|
17
|
+
subnet_mask: data.dig("ipv4", "status", "subnetMask"),
|
|
18
|
+
gateway: data.dig("ipv4", "status", "gateway")
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Switch eth0 to a static address. All three values are required: the PDU
|
|
23
|
+
# applies them together, and a wrong mask strands it on the wrong subnet.
|
|
24
|
+
def set_ipv4(address:, subnet_mask:, gateway:)
|
|
25
|
+
raise ArgumentError, "address, subnet_mask and gateway are all required" if
|
|
26
|
+
[address, subnet_mask, gateway].any? { |v| v.to_s.strip.empty? }
|
|
27
|
+
|
|
28
|
+
put(ETH0_PATH, ipv4: {
|
|
29
|
+
settings: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
mode: "manual",
|
|
32
|
+
manual: { address: address, subnetMask: subnet_mask, gateway: gateway }
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/eaton/power.rb
CHANGED
|
@@ -4,14 +4,14 @@ module Eaton
|
|
|
4
4
|
module Power
|
|
5
5
|
# Get overall power consumption for the PDU
|
|
6
6
|
# Returns power in watts
|
|
7
|
-
def
|
|
7
|
+
def power
|
|
8
8
|
data = get("/powerDistributions/1/inputs/1")
|
|
9
9
|
data.dig("measures", "activePower")
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# Get per-outlet power consumption
|
|
13
13
|
# Returns an array of hashes with outlet info and power in watts
|
|
14
|
-
def
|
|
14
|
+
def outlets
|
|
15
15
|
# Get list of outlets
|
|
16
16
|
outlets_list = get("/powerDistributions/1/outlets")
|
|
17
17
|
member_count = outlets_list["members@count"] || 0
|
|
@@ -41,7 +41,7 @@ module Eaton
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Get detailed power information including voltage, current, and power factor
|
|
44
|
-
def
|
|
44
|
+
def detailed
|
|
45
45
|
input_data = get("/powerDistributions/1/inputs/1")
|
|
46
46
|
|
|
47
47
|
{
|
|
@@ -55,13 +55,13 @@ module Eaton
|
|
|
55
55
|
cumulated_energy: input_data.dig("measures", "cumulatedEnergy"),
|
|
56
56
|
partial_energy: input_data.dig("measures", "partialEnergy")
|
|
57
57
|
},
|
|
58
|
-
outlets:
|
|
58
|
+
outlets: outlets
|
|
59
59
|
}
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Get branch power information
|
|
63
63
|
# Returns an array of branch power data
|
|
64
|
-
def
|
|
64
|
+
def branches
|
|
65
65
|
branches_list = get("/powerDistributions/1/branches")
|
|
66
66
|
member_count = branches_list["members@count"] || 0
|
|
67
67
|
|
|
@@ -87,7 +87,7 @@ module Eaton
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
# Get PDU information
|
|
90
|
-
def
|
|
90
|
+
def info
|
|
91
91
|
data = get("/powerDistributions/1")
|
|
92
92
|
|
|
93
93
|
{
|
data/lib/eaton/version.rb
CHANGED
data/lib/eaton.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eaton
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Siegel
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: thor
|
|
@@ -29,7 +28,7 @@ description: Comprehensive power monitoring and management for Eaton Rack PDU G4
|
|
|
29
28
|
detailed metrics (voltage, current, power factor), OAuth2 authentication, and SSH
|
|
30
29
|
tunneling support.
|
|
31
30
|
email:
|
|
32
|
-
-
|
|
31
|
+
- "<248302+usiegj00@users.noreply.github.com>"
|
|
33
32
|
executables:
|
|
34
33
|
- eaton
|
|
35
34
|
extensions: []
|
|
@@ -39,10 +38,12 @@ files:
|
|
|
39
38
|
- LICENSE
|
|
40
39
|
- README.md
|
|
41
40
|
- Rakefile
|
|
41
|
+
- TODO.md
|
|
42
42
|
- exe/eaton
|
|
43
43
|
- lib/eaton.rb
|
|
44
44
|
- lib/eaton/cli.rb
|
|
45
45
|
- lib/eaton/client.rb
|
|
46
|
+
- lib/eaton/network.rb
|
|
46
47
|
- lib/eaton/power.rb
|
|
47
48
|
- lib/eaton/version.rb
|
|
48
49
|
- sig/pdu_manager.rbs
|
|
@@ -55,7 +56,6 @@ metadata:
|
|
|
55
56
|
bug_tracker_uri: https://github.com/usiegj00/eaton/issues
|
|
56
57
|
changelog_uri: https://github.com/usiegj00/eaton/blob/main/CHANGELOG.md
|
|
57
58
|
documentation_uri: https://github.com/usiegj00/eaton
|
|
58
|
-
post_install_message:
|
|
59
59
|
rdoc_options: []
|
|
60
60
|
require_paths:
|
|
61
61
|
- lib
|
|
@@ -70,8 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
70
|
- !ruby/object:Gem::Version
|
|
71
71
|
version: '0'
|
|
72
72
|
requirements: []
|
|
73
|
-
rubygems_version: 3.
|
|
74
|
-
signing_key:
|
|
73
|
+
rubygems_version: 3.6.9
|
|
75
74
|
specification_version: 4
|
|
76
75
|
summary: Ruby gem and CLI for managing Eaton Rack PDU G4 devices via REST API
|
|
77
76
|
test_files: []
|