eaton 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +269 -0
- data/Rakefile +8 -0
- data/exe/eaton +5 -0
- data/lib/eaton/cli.rb +161 -0
- data/lib/eaton/client.rb +144 -0
- data/lib/eaton/power.rb +109 -0
- data/lib/eaton/version.rb +5 -0
- data/lib/eaton.rb +10 -0
- data/sig/pdu_manager.rbs +4 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 42cf738c337ad3fae01953c5e78003e8ed0c789146ee7b71bacaf8f3162b98ae
|
|
4
|
+
data.tar.gz: 0ebf36ddf22c38dea0a216777f8b04fb5da841648cbc1ccc0e4a83a8f45427ff
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5d342a6f0162da2fc073ccdb52f768a98daa7a6486b1c327c261b0a4b88e41c3d4d3763f55b02a14fb24e4393ff89d95f4d88f5c49ce728ad95c5fd78e98adbb
|
|
7
|
+
data.tar.gz: 26622a5ed0fcd33eb9249b25f92550b887f6844c528fd9641380782aabdbe3f4de5717020d6554d0212640b1aeffd6853963e6b3038c5757e3962e6245fbceed
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-01-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release
|
|
12
|
+
- OAuth2 bearer token authentication with Eaton PDU G4 devices
|
|
13
|
+
- Power monitoring commands:
|
|
14
|
+
- `eaton power` - Overall power consumption
|
|
15
|
+
- `eaton outlets` - Per-outlet power monitoring
|
|
16
|
+
- `eaton branches` - Branch power distribution
|
|
17
|
+
- `eaton detailed` - Detailed power metrics
|
|
18
|
+
- `eaton info` - PDU device information
|
|
19
|
+
- `eaton auth` - Authentication testing
|
|
20
|
+
- Smart filtering: text mode shows only active outlets/branches, JSON shows all
|
|
21
|
+
- SSH tunneling support via custom host headers
|
|
22
|
+
- Dual output formats: human-friendly text and machine-readable JSON
|
|
23
|
+
- Ruby API for programmatic access
|
|
24
|
+
- Support for 42 outlets and 6 branches
|
|
25
|
+
- Metrics include: watts, voltage, current, power factor, frequency, load percentage
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
- Zero-dependency HTTP client (uses Ruby's Net::HTTP)
|
|
29
|
+
- Automatic token management and session cleanup
|
|
30
|
+
- Comprehensive error handling
|
|
31
|
+
- CLI built with Thor
|
|
32
|
+
- Compatible with Ruby 3.0+
|
|
33
|
+
|
|
34
|
+
[0.1.0]: https://github.com/usiegj00/eaton/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jonathan Siegel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Eaton PDU Manager
|
|
2
|
+
|
|
3
|
+
A Ruby gem and CLI for managing Eaton Rack PDU G4 devices via REST API. Provides comprehensive power monitoring and management capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔌 **Power Monitoring**: Overall, per-outlet, and per-branch power consumption
|
|
8
|
+
- 📊 **Detailed Metrics**: Voltage, current, power factor, frequency, load percentage
|
|
9
|
+
- 🔄 **Smart Filtering**: Text mode shows only active outlets/branches, JSON mode shows all
|
|
10
|
+
- 🔐 **OAuth2 Authentication**: Secure bearer token authentication
|
|
11
|
+
- 🌐 **SSH Tunneling**: Support for SSH tunneled connections via custom host headers
|
|
12
|
+
- 📝 **Dual Output**: Human-friendly text or machine-readable JSON
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'eaton'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install directly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install eaton
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For development:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/usiegj00/eaton.git
|
|
32
|
+
cd eaton
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Get overall power consumption
|
|
40
|
+
eaton power \
|
|
41
|
+
--host pdu.example.com \
|
|
42
|
+
--username admin \
|
|
43
|
+
--password your_password
|
|
44
|
+
|
|
45
|
+
# Get active outlets only (text mode)
|
|
46
|
+
eaton outlets \
|
|
47
|
+
--host pdu.example.com \
|
|
48
|
+
--username admin \
|
|
49
|
+
--password your_password
|
|
50
|
+
|
|
51
|
+
# Get all outlets as JSON
|
|
52
|
+
eaton outlets \
|
|
53
|
+
--host pdu.example.com \
|
|
54
|
+
--username admin \
|
|
55
|
+
--password your_password \
|
|
56
|
+
--format json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
All commands support these options:
|
|
62
|
+
- `--host` - PDU hostname or IP (required)
|
|
63
|
+
- `--port` - PDU port (default: 443)
|
|
64
|
+
- `--username` - PDU username (required)
|
|
65
|
+
- `--password` - PDU password (required)
|
|
66
|
+
- `--host-header` - Custom Host header for SSH tunneling (optional)
|
|
67
|
+
- `--verify-ssl` - Verify SSL certificates (default: false)
|
|
68
|
+
- `--format` - Output format: `text` or `json` (default: text)
|
|
69
|
+
|
|
70
|
+
### Available Commands
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
|---------|-------------|
|
|
74
|
+
| `eaton auth` | Test authentication |
|
|
75
|
+
| `eaton info` | Display PDU device information |
|
|
76
|
+
| `eaton power` | Get overall power consumption (watts) |
|
|
77
|
+
| `eaton outlets` | Get per-outlet power consumption |
|
|
78
|
+
| `eaton branches` | Get per-branch power distribution |
|
|
79
|
+
| `eaton detailed` | Get detailed power metrics |
|
|
80
|
+
|
|
81
|
+
### Output Filtering
|
|
82
|
+
|
|
83
|
+
**Text Mode** (default):
|
|
84
|
+
- Shows only outlets/branches with active power draw
|
|
85
|
+
- Clean, focused output for human reading
|
|
86
|
+
|
|
87
|
+
**JSON Mode** (`--format json`):
|
|
88
|
+
- Shows all outlets/branches regardless of state
|
|
89
|
+
- Complete data for automation and monitoring systems
|
|
90
|
+
|
|
91
|
+
## Usage Examples
|
|
92
|
+
|
|
93
|
+
### Get PDU Information
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
eaton info --host pdu.example.com --username admin --password secret
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Output:
|
|
100
|
+
```
|
|
101
|
+
PDU Device Information:
|
|
102
|
+
============================================================
|
|
103
|
+
id: 1
|
|
104
|
+
name: PDU
|
|
105
|
+
model: Eaton Rack PDU G4
|
|
106
|
+
serial_number: ABC123
|
|
107
|
+
vendor: Eaton
|
|
108
|
+
firmware_version: 2.9.2
|
|
109
|
+
status: in service
|
|
110
|
+
health: ok
|
|
111
|
+
nominal_power: 19800
|
|
112
|
+
nominal_current: 55
|
|
113
|
+
nominal_voltage: 208
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Monitor Overall Power
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
eaton power --host pdu.example.com --username admin --password secret
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Output:
|
|
123
|
+
```
|
|
124
|
+
Overall Power:
|
|
125
|
+
============================================================
|
|
126
|
+
watts: 1542.3
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### View Active Outlets
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
eaton outlets --host pdu.example.com --username admin --password secret
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Shows only outlets currently drawing power.
|
|
136
|
+
|
|
137
|
+
### Export All Outlets to JSON
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
eaton outlets \
|
|
141
|
+
--host pdu.example.com \
|
|
142
|
+
--username admin \
|
|
143
|
+
--password secret \
|
|
144
|
+
--format json > outlets.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### SSH Tunneling
|
|
148
|
+
|
|
149
|
+
When connecting through an SSH tunnel:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# SSH tunnel to remote PDU
|
|
153
|
+
ssh -L 5000:192.168.1.100:443 user@jumphost
|
|
154
|
+
|
|
155
|
+
# Connect via tunnel with custom host header
|
|
156
|
+
eaton power \
|
|
157
|
+
--host localhost \
|
|
158
|
+
--port 5000 \
|
|
159
|
+
--username admin \
|
|
160
|
+
--password secret \
|
|
161
|
+
--host-header 192.168.1.100
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Ruby API
|
|
165
|
+
|
|
166
|
+
Use the gem programmatically in your Ruby code:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
require 'eaton'
|
|
170
|
+
|
|
171
|
+
# Create client
|
|
172
|
+
client = Eaton::Client.new(
|
|
173
|
+
host: 'pdu.example.com',
|
|
174
|
+
username: 'admin',
|
|
175
|
+
password: 'secret',
|
|
176
|
+
verify_ssl: true
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Extend with power monitoring
|
|
180
|
+
client.extend(Eaton::Power)
|
|
181
|
+
|
|
182
|
+
# Get PDU info
|
|
183
|
+
info = client.pdu_info
|
|
184
|
+
puts "#{info[:model]} - #{info[:serial_number]}"
|
|
185
|
+
|
|
186
|
+
# Get overall power
|
|
187
|
+
power = client.overall_power
|
|
188
|
+
puts "Current draw: #{power} watts"
|
|
189
|
+
|
|
190
|
+
# Get active outlets
|
|
191
|
+
outlets = client.outlet_power
|
|
192
|
+
outlets.select { |o| o[:watts] > 0 }.each do |outlet|
|
|
193
|
+
puts "#{outlet[:name]}: #{outlet[:watts]}W"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Get branch distribution
|
|
197
|
+
branches = client.branch_power
|
|
198
|
+
branches.each do |branch|
|
|
199
|
+
puts "#{branch[:name]}: #{branch[:current]}A @ #{branch[:voltage]}V"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Clean up
|
|
203
|
+
client.logout
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## API Endpoints
|
|
207
|
+
|
|
208
|
+
The gem interfaces with these REST API endpoints:
|
|
209
|
+
|
|
210
|
+
- `/powerDistributions/1` - PDU device information
|
|
211
|
+
- `/powerDistributions/1/inputs/1` - Overall power input
|
|
212
|
+
- `/powerDistributions/1/outlets/{id}` - Individual outlet data
|
|
213
|
+
- `/powerDistributions/1/branches/{id}` - Branch distribution data
|
|
214
|
+
|
|
215
|
+
## Supported Devices
|
|
216
|
+
|
|
217
|
+
Tested and verified with:
|
|
218
|
+
- **Eaton Rack PDU G4**
|
|
219
|
+
- Firmware: 2.9.2+
|
|
220
|
+
- API: `/rest/mbdetnrs/2.0`
|
|
221
|
+
|
|
222
|
+
Should work with other Eaton PDU models using the same API version.
|
|
223
|
+
|
|
224
|
+
## Authentication
|
|
225
|
+
|
|
226
|
+
Uses OAuth2 bearer token authentication:
|
|
227
|
+
|
|
228
|
+
1. POST credentials to `/oauth2/token/`
|
|
229
|
+
2. Receive access token
|
|
230
|
+
3. Include token in `Authorization: Bearer {token}` header
|
|
231
|
+
4. Token automatically managed and refreshed
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# Clone repository
|
|
237
|
+
git clone https://github.com/usiegj00/eaton.git
|
|
238
|
+
cd eaton
|
|
239
|
+
|
|
240
|
+
# Install dependencies
|
|
241
|
+
bundle install
|
|
242
|
+
|
|
243
|
+
# Run tests
|
|
244
|
+
bundle exec rspec
|
|
245
|
+
|
|
246
|
+
# Install locally
|
|
247
|
+
bundle exec rake install
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Contributing
|
|
251
|
+
|
|
252
|
+
1. Fork it
|
|
253
|
+
2. Create your feature branch (`git checkout -b feature/my-feature`)
|
|
254
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
255
|
+
4. Push to the branch (`git push origin feature/my-feature`)
|
|
256
|
+
5. Create a Pull Request
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT License - see LICENSE file for details
|
|
261
|
+
|
|
262
|
+
## Support
|
|
263
|
+
|
|
264
|
+
- Issues: https://github.com/usiegj00/eaton/issues
|
|
265
|
+
- Documentation: https://github.com/usiegj00/eaton
|
|
266
|
+
|
|
267
|
+
## Credits
|
|
268
|
+
|
|
269
|
+
Developed for managing Eaton Rack PDU G4 devices via REST API.
|
data/Rakefile
ADDED
data/exe/eaton
ADDED
data/lib/eaton/cli.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Eaton
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
class_option :host, type: :string, required: true, desc: "PDU hostname or IP address"
|
|
9
|
+
class_option :port, type: :numeric, default: 443, desc: "PDU port (default: 443)"
|
|
10
|
+
class_option :username, type: :string, required: true, desc: "PDU username"
|
|
11
|
+
class_option :password, type: :string, required: true, desc: "PDU password"
|
|
12
|
+
class_option :verify_ssl, type: :boolean, default: false, desc: "Verify SSL certificates"
|
|
13
|
+
class_option :host_header, type: :string, desc: "Custom Host header (for SSH tunneling)"
|
|
14
|
+
class_option :format, type: :string, default: "text", enum: ["text", "json"], desc: "Output format"
|
|
15
|
+
|
|
16
|
+
desc "power", "Get overall power consumption in watts"
|
|
17
|
+
def power
|
|
18
|
+
with_client do |client|
|
|
19
|
+
power = client.overall_power
|
|
20
|
+
output_result("Overall Power", { watts: power })
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "outlets", "Get per-outlet power consumption"
|
|
25
|
+
def outlets
|
|
26
|
+
with_client do |client|
|
|
27
|
+
outlets = client.outlet_power
|
|
28
|
+
# Filter out zero-power outlets in text mode
|
|
29
|
+
if options[:format] == "text"
|
|
30
|
+
outlets = outlets.select { |o| o[:watts] && o[:watts] > 0 }
|
|
31
|
+
end
|
|
32
|
+
output_result("Outlet Power", outlets)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "detailed", "Get detailed power information"
|
|
37
|
+
def detailed
|
|
38
|
+
with_client do |client|
|
|
39
|
+
info = client.detailed_power_info
|
|
40
|
+
# Filter outlets in text mode
|
|
41
|
+
if options[:format] == "text" && info[:outlets]
|
|
42
|
+
info[:outlets] = info[:outlets].select { |o| o[:watts] && o[:watts] > 0 }
|
|
43
|
+
end
|
|
44
|
+
output_result("Detailed Power Information", info)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc "branches", "Get power consumption per branch"
|
|
49
|
+
def branches
|
|
50
|
+
with_client do |client|
|
|
51
|
+
branches = client.branch_power
|
|
52
|
+
# Filter out zero-current branches in text mode
|
|
53
|
+
if options[:format] == "text"
|
|
54
|
+
branches = branches.select { |b| b[:current] && b[:current] > 0 }
|
|
55
|
+
end
|
|
56
|
+
output_result("Branch Power", branches)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "info", "Display PDU device information"
|
|
61
|
+
def info
|
|
62
|
+
with_client do |client|
|
|
63
|
+
info = client.pdu_info
|
|
64
|
+
output_result("PDU Device Information", info)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "auth", "Test authentication with the PDU"
|
|
69
|
+
def auth
|
|
70
|
+
with_client do |client|
|
|
71
|
+
token = client.authenticate!
|
|
72
|
+
output_result("Authentication Test", {
|
|
73
|
+
status: "success",
|
|
74
|
+
token_present: !token.nil?,
|
|
75
|
+
token_length: token&.length
|
|
76
|
+
})
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
no_commands do
|
|
81
|
+
def with_client
|
|
82
|
+
client = Client.new(
|
|
83
|
+
host: options[:host],
|
|
84
|
+
port: options[:port],
|
|
85
|
+
username: options[:username],
|
|
86
|
+
password: options[:password],
|
|
87
|
+
verify_ssl: options[:verify_ssl],
|
|
88
|
+
host_header: options[:host_header]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Mix in the Power module to add power monitoring methods
|
|
92
|
+
client.extend(Power)
|
|
93
|
+
|
|
94
|
+
yield client
|
|
95
|
+
rescue Client::AuthenticationError => e
|
|
96
|
+
error("Authentication failed: #{e.message}")
|
|
97
|
+
rescue Client::APIError => e
|
|
98
|
+
error("API error: #{e.message}")
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
error("Unexpected error: #{e.message}")
|
|
101
|
+
ensure
|
|
102
|
+
client&.logout
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def output_result(title, data)
|
|
106
|
+
if options[:format] == "json"
|
|
107
|
+
puts JSON.pretty_generate(data)
|
|
108
|
+
else
|
|
109
|
+
output_text(title, data)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def output_text(title, data)
|
|
114
|
+
puts "\n#{title}:"
|
|
115
|
+
puts "=" * 60
|
|
116
|
+
|
|
117
|
+
case data
|
|
118
|
+
when Hash
|
|
119
|
+
output_hash(data)
|
|
120
|
+
when Array
|
|
121
|
+
data.each_with_index do |item, index|
|
|
122
|
+
puts "\n[#{index + 1}]"
|
|
123
|
+
output_hash(item) if item.is_a?(Hash)
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
puts data
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
puts
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def output_hash(hash, indent = 0)
|
|
133
|
+
hash.each do |key, value|
|
|
134
|
+
prefix = " " * indent
|
|
135
|
+
case value
|
|
136
|
+
when Hash
|
|
137
|
+
puts "#{prefix}#{key}:"
|
|
138
|
+
output_hash(value, indent + 1)
|
|
139
|
+
when Array
|
|
140
|
+
puts "#{prefix}#{key}:"
|
|
141
|
+
value.each_with_index do |item, index|
|
|
142
|
+
if item.is_a?(Hash)
|
|
143
|
+
puts "#{prefix} [#{index}]:"
|
|
144
|
+
output_hash(item, indent + 2)
|
|
145
|
+
else
|
|
146
|
+
puts "#{prefix} - #{item}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
puts "#{prefix}#{key}: #{value}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def error(message)
|
|
156
|
+
STDERR.puts "ERROR: #{message}"
|
|
157
|
+
exit 1
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
data/lib/eaton/client.rb
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "openssl"
|
|
7
|
+
|
|
8
|
+
module Eaton
|
|
9
|
+
class Client
|
|
10
|
+
class AuthenticationError < StandardError; end
|
|
11
|
+
class APIError < StandardError; end
|
|
12
|
+
|
|
13
|
+
attr_reader :host, :username, :base_url
|
|
14
|
+
|
|
15
|
+
def initialize(host:, username:, password:, port: 443, verify_ssl: false, host_header: nil)
|
|
16
|
+
@host = host
|
|
17
|
+
@username = username
|
|
18
|
+
@password = password
|
|
19
|
+
@port = port
|
|
20
|
+
@verify_ssl = verify_ssl
|
|
21
|
+
@host_header = host_header || host
|
|
22
|
+
@base_path = "/rest/mbdetnrs/2.0"
|
|
23
|
+
@token = nil
|
|
24
|
+
@session = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def authenticate!
|
|
28
|
+
request = Net::HTTP::Post.new("#{@base_path}/oauth2/token/")
|
|
29
|
+
add_browser_headers(request)
|
|
30
|
+
request.body = JSON.generate(username: @username, password: @password)
|
|
31
|
+
|
|
32
|
+
response = execute_request(request)
|
|
33
|
+
|
|
34
|
+
if response.code.to_i.between?(200, 299)
|
|
35
|
+
data = JSON.parse(response.body)
|
|
36
|
+
@token = data["access_token"]
|
|
37
|
+
@session = data["session"]
|
|
38
|
+
@token
|
|
39
|
+
else
|
|
40
|
+
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
41
|
+
end
|
|
42
|
+
rescue JSON::ParserError => e
|
|
43
|
+
raise AuthenticationError, "Invalid response from server: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def authenticated?
|
|
47
|
+
!@token.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(path)
|
|
51
|
+
authenticate! unless authenticated?
|
|
52
|
+
|
|
53
|
+
request = Net::HTTP::Get.new("#{@base_path}#{path}")
|
|
54
|
+
add_auth_headers(request)
|
|
55
|
+
|
|
56
|
+
handle_response(execute_request(request))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def post(path, data = {})
|
|
60
|
+
authenticate! unless authenticated?
|
|
61
|
+
|
|
62
|
+
request = Net::HTTP::Post.new("#{@base_path}#{path}")
|
|
63
|
+
add_auth_headers(request)
|
|
64
|
+
request.body = data.to_json
|
|
65
|
+
|
|
66
|
+
handle_response(execute_request(request))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def logout
|
|
70
|
+
return unless authenticated?
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
delete(@session) if @session
|
|
74
|
+
rescue APIError
|
|
75
|
+
# Session might already be expired or deleted, ignore
|
|
76
|
+
ensure
|
|
77
|
+
@token = nil
|
|
78
|
+
@session = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def http_connection
|
|
85
|
+
@http_connection ||= begin
|
|
86
|
+
http = Net::HTTP.new(@host, @port)
|
|
87
|
+
http.use_ssl = true
|
|
88
|
+
http.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
89
|
+
http
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def execute_request(request)
|
|
94
|
+
http_connection.request(request)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_browser_headers(request)
|
|
98
|
+
request["Content-Type"] = "application/json"
|
|
99
|
+
request["Host"] = @host_header
|
|
100
|
+
request["Sec-Fetch-Mode"] = "cors"
|
|
101
|
+
request["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"
|
|
102
|
+
request["Origin"] = "https://#{@host_header}"
|
|
103
|
+
request["Sec-Fetch-Site"] = "same-origin"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_auth_headers(request)
|
|
107
|
+
request["Content-Type"] = "application/json"
|
|
108
|
+
request["Authorization"] = "Bearer #{@token}"
|
|
109
|
+
request["Host"] = @host_header
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def delete(path)
|
|
113
|
+
request = Net::HTTP::Delete.new("#{@base_path}#{path}")
|
|
114
|
+
add_auth_headers(request)
|
|
115
|
+
|
|
116
|
+
handle_response(execute_request(request))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_response(response)
|
|
120
|
+
status_code = response.code.to_i
|
|
121
|
+
|
|
122
|
+
case status_code
|
|
123
|
+
when 200..299
|
|
124
|
+
begin
|
|
125
|
+
JSON.parse(response.body)
|
|
126
|
+
rescue JSON::ParserError
|
|
127
|
+
response.body
|
|
128
|
+
end
|
|
129
|
+
when 401, 403
|
|
130
|
+
# Token might have expired, clear it and let caller retry
|
|
131
|
+
@token = nil
|
|
132
|
+
raise AuthenticationError, "Authentication failed or token expired"
|
|
133
|
+
else
|
|
134
|
+
error_message = begin
|
|
135
|
+
data = JSON.parse(response.body)
|
|
136
|
+
data["description"] || response.body
|
|
137
|
+
rescue
|
|
138
|
+
response.body
|
|
139
|
+
end
|
|
140
|
+
raise APIError, "API error (#{status_code}): #{error_message}"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/eaton/power.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Eaton
|
|
4
|
+
module Power
|
|
5
|
+
# Get overall power consumption for the PDU
|
|
6
|
+
# Returns power in watts
|
|
7
|
+
def overall_power
|
|
8
|
+
data = get("/powerDistributions/1/inputs/1")
|
|
9
|
+
data.dig("measures", "activePower")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get per-outlet power consumption
|
|
13
|
+
# Returns an array of hashes with outlet info and power in watts
|
|
14
|
+
def outlet_power
|
|
15
|
+
# Get list of outlets
|
|
16
|
+
outlets_list = get("/powerDistributions/1/outlets")
|
|
17
|
+
member_count = outlets_list["members@count"] || 0
|
|
18
|
+
|
|
19
|
+
return [] if member_count.zero?
|
|
20
|
+
|
|
21
|
+
# Get data for each outlet
|
|
22
|
+
outlets = []
|
|
23
|
+
outlets_list["members"].each do |member|
|
|
24
|
+
outlet_id = member["@id"].split("/").last
|
|
25
|
+
outlet_data = get("/powerDistributions/1/outlets/#{outlet_id}")
|
|
26
|
+
|
|
27
|
+
outlets << {
|
|
28
|
+
id: outlet_data["id"],
|
|
29
|
+
name: outlet_data.dig("identification", "friendlyName") || "Outlet #{outlet_id}",
|
|
30
|
+
physical_name: outlet_data.dig("identification", "physicalName"),
|
|
31
|
+
watts: outlet_data.dig("measures", "activePower"),
|
|
32
|
+
current: outlet_data.dig("measures", "current"),
|
|
33
|
+
voltage: nil, # Outlets don't report voltage individually
|
|
34
|
+
power_factor: outlet_data.dig("measures", "powerFactor"),
|
|
35
|
+
state: outlet_data.dig("status", "switchedOn") ? "on" : "off",
|
|
36
|
+
switched_on: outlet_data.dig("status", "switchedOn")
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
outlets
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get detailed power information including voltage, current, and power factor
|
|
44
|
+
def detailed_power_info
|
|
45
|
+
input_data = get("/powerDistributions/1/inputs/1")
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
overall: {
|
|
49
|
+
watts: input_data.dig("measures", "activePower"),
|
|
50
|
+
apparent_power: input_data.dig("measures", "apparentPower"),
|
|
51
|
+
reactive_power: input_data.dig("measures", "reactivePower"),
|
|
52
|
+
frequency: input_data.dig("measures", "frequency"),
|
|
53
|
+
power_factor: input_data.dig("measures", "powerFactor"),
|
|
54
|
+
percent_load: input_data.dig("measures", "percentLoad"),
|
|
55
|
+
cumulated_energy: input_data.dig("measures", "cumulatedEnergy"),
|
|
56
|
+
partial_energy: input_data.dig("measures", "partialEnergy")
|
|
57
|
+
},
|
|
58
|
+
outlets: outlet_power
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get branch power information
|
|
63
|
+
# Returns an array of branch power data
|
|
64
|
+
def branch_power
|
|
65
|
+
branches_list = get("/powerDistributions/1/branches")
|
|
66
|
+
member_count = branches_list["members@count"] || 0
|
|
67
|
+
|
|
68
|
+
return [] if member_count.zero?
|
|
69
|
+
|
|
70
|
+
branches = []
|
|
71
|
+
branches_list["members"].each do |member|
|
|
72
|
+
branch_id = member["@id"].split("/").last
|
|
73
|
+
branch_data = get("/powerDistributions/1/branches/#{branch_id}")
|
|
74
|
+
|
|
75
|
+
branches << {
|
|
76
|
+
id: branch_data["id"],
|
|
77
|
+
name: branch_data.dig("identification", "friendlyName") || "Branch #{branch_id}",
|
|
78
|
+
physical_name: branch_data.dig("identification", "physicalName"),
|
|
79
|
+
watts: branch_data.dig("measures", "activePower"),
|
|
80
|
+
current: branch_data.dig("measures", "current"),
|
|
81
|
+
voltage: branch_data.dig("measures", "voltage"),
|
|
82
|
+
power_factor: branch_data.dig("measures", "powerFactor")
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
branches
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get PDU information
|
|
90
|
+
def pdu_info
|
|
91
|
+
data = get("/powerDistributions/1")
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
id: data["id"],
|
|
95
|
+
name: data.dig("identification", "friendlyName"),
|
|
96
|
+
model: data.dig("identification", "model"),
|
|
97
|
+
serial_number: data.dig("identification", "serialNumber"),
|
|
98
|
+
part_number: data.dig("identification", "partNumber"),
|
|
99
|
+
vendor: data.dig("identification", "vendor"),
|
|
100
|
+
firmware_version: data.dig("identification", "firmwareVersion"),
|
|
101
|
+
status: data.dig("status", "operating"),
|
|
102
|
+
health: data.dig("status", "health"),
|
|
103
|
+
nominal_power: data.dig("specifications", "activePower", "nominal"),
|
|
104
|
+
nominal_current: data.dig("specifications", "current", "nominal"),
|
|
105
|
+
nominal_voltage: data.dig("specifications", "voltage", "nominal")
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/eaton.rb
ADDED
data/sig/pdu_manager.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eaton
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jonathan Siegel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
description: Comprehensive power monitoring and management for Eaton Rack PDU G4 devices.
|
|
28
|
+
Features include overall power consumption, per-outlet monitoring, branch distribution,
|
|
29
|
+
detailed metrics (voltage, current, power factor), OAuth2 authentication, and SSH
|
|
30
|
+
tunneling support.
|
|
31
|
+
email:
|
|
32
|
+
- jonathan@example.com
|
|
33
|
+
executables:
|
|
34
|
+
- eaton
|
|
35
|
+
extensions: []
|
|
36
|
+
extra_rdoc_files: []
|
|
37
|
+
files:
|
|
38
|
+
- CHANGELOG.md
|
|
39
|
+
- LICENSE
|
|
40
|
+
- README.md
|
|
41
|
+
- Rakefile
|
|
42
|
+
- exe/eaton
|
|
43
|
+
- lib/eaton.rb
|
|
44
|
+
- lib/eaton/cli.rb
|
|
45
|
+
- lib/eaton/client.rb
|
|
46
|
+
- lib/eaton/power.rb
|
|
47
|
+
- lib/eaton/version.rb
|
|
48
|
+
- sig/pdu_manager.rbs
|
|
49
|
+
homepage: https://github.com/usiegj00/eaton
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: https://github.com/usiegj00/eaton
|
|
54
|
+
source_code_uri: https://github.com/usiegj00/eaton
|
|
55
|
+
bug_tracker_uri: https://github.com/usiegj00/eaton/issues
|
|
56
|
+
changelog_uri: https://github.com/usiegj00/eaton/blob/main/CHANGELOG.md
|
|
57
|
+
documentation_uri: https://github.com/usiegj00/eaton
|
|
58
|
+
post_install_message:
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 3.0.0
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.5.22
|
|
74
|
+
signing_key:
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Ruby gem and CLI for managing Eaton Rack PDU G4 devices via REST API
|
|
77
|
+
test_files: []
|