eaton 0.2.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/TODO.md +81 -0
- data/lib/eaton/client.rb +71 -1
- data/lib/eaton/network.rb +37 -0
- data/lib/eaton/version.rb +1 -1
- data/lib/eaton.rb +1 -0
- 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/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/client.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Eaton
|
|
|
11
11
|
class APIError < StandardError; end
|
|
12
12
|
|
|
13
13
|
include Power
|
|
14
|
+
include Network
|
|
14
15
|
|
|
15
16
|
attr_reader :host, :username, :base_url
|
|
16
17
|
|
|
@@ -39,7 +40,7 @@ module Eaton
|
|
|
39
40
|
@session = data["session"]
|
|
40
41
|
@token
|
|
41
42
|
else
|
|
42
|
-
|
|
43
|
+
handle_auth_error(response)
|
|
43
44
|
end
|
|
44
45
|
rescue JSON::ParserError => e
|
|
45
46
|
raise AuthenticationError, "Invalid response from server: #{e.message}"
|
|
@@ -68,6 +69,16 @@ module Eaton
|
|
|
68
69
|
handle_response(execute_request(request))
|
|
69
70
|
end
|
|
70
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
|
+
|
|
71
82
|
def logout
|
|
72
83
|
return unless authenticated?
|
|
73
84
|
|
|
@@ -83,6 +94,65 @@ module Eaton
|
|
|
83
94
|
|
|
84
95
|
private
|
|
85
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
|
+
|
|
86
156
|
def http_connection
|
|
87
157
|
@http_connection ||= begin
|
|
88
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/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: []
|