Skip to content

Commit 5c5f1f0

Browse files
authored
feat: Add pdsh-style hostlist expression support (#107)
* feat: Add pdsh-style hostlist expression support (#98) Implement range expansion syntax for specifying multiple hosts: - Simple ranges: node[1-5] -> node1, node2, node3, node4, node5 - Zero-padded: node[01-05] -> node01, node02, node03, node04, node05 - Comma-separated: node[1,3,5] -> node1, node3, node5 - Mixed: node[1-3,7] -> node1, node2, node3, node7 - Cartesian product: rack[1-2]-node[1-3] -> 6 hosts - File input: ^/path/to/file reads hosts from file Integrate with -H option, --filter, and --exclude options. Add 52 unit tests for hostlist module. * fix: Address PR review findings - resource limits and code cleanup (#108) * fix: Address PR review findings - resource limits and code cleanup HIGH Priority Fixes: - Add resource exhaustion protection in parse_hostfile(): * Maximum file size limit of 1 MB * Maximum line count limit of 100,000 lines * Check file size before reading to prevent DoS attacks MEDIUM Priority Fixes: - Remove code duplication: * Move is_hostlist_expression() and looks_like_hostlist_range() from src/main.rs and src/app/nodes.rs to src/hostlist/mod.rs * Export functions as public API from hostlist module * Update all call sites to use the exported functions - Remove unused variable: * Remove unused 'sign' variable from parse_number() in parser.rs - Add overflow protection: * Use checked_mul() for cartesian product allocations in expander.rs * Return RangeTooLarge error if overflow would occur * Prevent integer overflow in intermediate calculations All changes compile successfully and pass tests (cargo test, cargo clippy). * docs: Add HOSTLIST EXPRESSIONS section and update option docs in manpage
1 parent 66b4e36 commit 5c5f1f0

File tree

11 files changed

+2068
-80
lines changed

11 files changed

+2068
-80
lines changed

ARCHITECTURE.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,82 @@ let mut cli = pdsh_cli.to_bssh_cli();
284284
- Unknown pdsh options produce helpful error messages
285285
- Normal bssh operation is completely unaffected by pdsh compat code
286286

287+
### 1.5 Hostlist Expression Support (`hostlist/*`)
288+
289+
**Module Structure (Added 2025-12-17, Issue #98):**
290+
- `hostlist/mod.rs` - Module exports and comma-separated pattern handling (130 lines)
291+
- `hostlist/parser.rs` - Range expression parser (570 lines)
292+
- `hostlist/expander.rs` - Range expansion and cartesian product (270 lines)
293+
- `hostlist/error.rs` - Error types with thiserror (80 lines)
294+
295+
**Design Decisions:**
296+
- pdsh-compatible hostlist expression syntax
297+
- Zero-cost abstraction for non-range patterns (pass-through)
298+
- Efficient cartesian product expansion for multiple ranges
299+
- Distinguishes hostlist expressions from glob patterns
300+
301+
**Hostlist Expression Syntax:**
302+
```
303+
hostlist = host_term (',' host_term)*
304+
host_term = prefix range_expr suffix
305+
range_expr = '[' range_list ']'
306+
range_list = range_item (',' range_item)*
307+
range_item = NUMBER | NUMBER '-' NUMBER
308+
prefix = STRING (any characters before '[')
309+
suffix = STRING (any characters after ']', may include nested ranges)
310+
```
311+
312+
**Supported Features:**
313+
- Simple range: `node[1-5]` -> `node1, node2, node3, node4, node5`
314+
- Zero-padded: `node[01-05]` -> `node01, node02, node03, node04, node05`
315+
- Comma-separated: `node[1,3,5]` -> `node1, node3, node5`
316+
- Mixed: `node[1-3,7,9-10]` -> 7 hosts
317+
- Cartesian product: `rack[1-2]-node[1-3]` -> 6 hosts
318+
- With domain: `web[1-3].example.com` -> 3 hosts
319+
- With user/port: `admin@db[01-03]:5432` -> 3 hosts with user and port
320+
- File input: `^/path/to/file` -> read hosts from file
321+
322+
**Integration Points:**
323+
- `-H` option in native bssh mode (all patterns automatically expanded)
324+
- `-w` option in pdsh compatibility mode
325+
- `--filter` option (supports both glob and hostlist patterns)
326+
- `--exclude` option (supports both glob and hostlist patterns)
327+
- pdsh query mode (`-q`) with full expansion support
328+
329+
**Pattern Detection Heuristics:**
330+
```rust
331+
// Distinguishes hostlist expressions from glob patterns
332+
// Hostlist: [1-5], [01-05], [1,2,3], [1-3,5-7] (numeric content)
333+
// Glob: [abc], [a-z], [!xyz] (alphabetic content)
334+
335+
fn is_hostlist_expression(pattern: &str) -> bool {
336+
// Check if brackets contain numeric ranges
337+
// Numeric: 1-5, 01-05, 1,2,3
338+
// Non-numeric (glob): abc, a-z, !xyz
339+
}
340+
```
341+
342+
**Safety Limits:**
343+
- Maximum expansion size: 100,000 hosts (prevents DoS)
344+
- Validates range direction (start <= end)
345+
- Error on empty brackets, unclosed brackets, nested brackets
346+
- IPv6 literal bracket disambiguation
347+
348+
**Data Flow:**
349+
```
350+
Input: "admin@web[1-3].example.com:22"
351+
352+
Parse user prefix: "admin@"
353+
354+
Parse hostname with range: "web[1-3].example.com"
355+
356+
Expand range: ["web1.example.com", "web2.example.com", "web3.example.com"]
357+
358+
Parse port suffix: ":22"
359+
360+
361+
```
362+
287363
### 2. Configuration Management (`config/*`)
288364

289365
**Module Structure (Refactored 2025-10-17):**

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A high-performance SSH client with **SSH-compatible syntax** for both single-hos
1515
- **Port Forwarding**: Full support for local (-L), remote (-R), and dynamic (-D) SSH port forwarding
1616
- **Jump Host Support**: Connect through bastion hosts using OpenSSH ProxyJump syntax (`-J`)
1717
- **Parallel Execution**: Execute commands across multiple nodes simultaneously
18+
- **Hostlist Expressions**: pdsh-style range expansion (`node[1-5]`, `rack[1-2]-node[1-3]`) for compact host specification
1819
- **Fail-Fast Mode**: Stop immediately on first failure with `-k` flag (pdsh compatible)
1920
- **Interactive Terminal UI (TUI)**: Real-time monitoring with 4 view modes (Summary/Detail/Split/Diff) for multi-node operations
2021
- **Cluster Management**: Define and manage node clusters via configuration files
@@ -179,14 +180,24 @@ bssh -H "[email protected],[email protected]:2222" "uptime"
179180
# Using cluster from config
180181
bssh -C production "df -h"
181182

183+
# Hostlist expressions (pdsh-style range expansion)
184+
bssh -H "node[1-5]" "uptime" # node1, node2, node3, node4, node5
185+
bssh -H "node[01-05]" "df -h" # Zero-padded: node01, node02, ...
186+
bssh -H "node[1,3,5]" "ps aux" # Specific values: node1, node3, node5
187+
bssh -H "rack[1-2]-node[1-3]" "uptime" # Cartesian product: 6 hosts
188+
bssh -H "web[1-3].example.com" "nginx -v" # With domain suffix
189+
bssh -H "admin@db[01-03]:5432" "psql --version" # With user and port
190+
bssh -H "^/etc/hosts.cluster" "uptime" # Read hosts from file
191+
182192
# Filter specific hosts with pattern matching
183193
bssh -H "web1,web2,db1,db2" -f "web*" "systemctl status nginx"
184194
bssh -C production -f "db*" "pg_dump --version"
195+
bssh -H "node[1-10]" -f "node[1-5]" "uptime" # Filter with hostlist expression
185196

186197
# Exclude specific hosts from execution
187198
bssh -H "node1,node2,node3" --exclude "node2" "uptime"
188199
bssh -C production --exclude "db*" "systemctl restart nginx"
189-
bssh -C production --exclude "web1,web2" "apt update"
200+
bssh -H "node[1-10]" --exclude "node[3-5]" "uptime" # Exclude with hostlist expression
190201

191202
# With custom SSH key
192203
bssh -C staging -i ~/.ssh/custom_key "systemctl status nginx"

docs/man/bssh.1

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,29 @@ Example: -D 1080 (SOCKS5 proxy on localhost:1080), -D *:1080/4 (SOCKS4 on all in
113113
.TP
114114
.BR \-H ", " \-\-hosts " " \fIHOSTS\fR
115115
Comma-separated list of hosts in [user@]hostname[:port] format.
116-
Example: user1@host1:2222,user2@host2
116+
Supports pdsh-style hostlist expressions for range expansion.
117+
.RS
118+
.PP
119+
Simple host list:
120+
.IP \[bu] 2
121+
-H "user1@host1:2222,user2@host2"
122+
.PP
123+
Hostlist expressions (range expansion):
124+
.IP \[bu] 2
125+
-H "node[1-5]" \[->] node1, node2, node3, node4, node5
126+
.IP \[bu] 2
127+
-H "node[01-05]" \[->] node01, node02, ... (zero-padded)
128+
.IP \[bu] 2
129+
-H "node[1,3,5]" \[->] node1, node3, node5 (specific values)
130+
.IP \[bu] 2
131+
-H "rack[1-2]-node[1-3]" \[->] 6 hosts (cartesian product)
132+
.IP \[bu] 2
133+
-H "web[1-3].example.com" \[->] web1.example.com, web2.example.com, ...
134+
.IP \[bu] 2
135+
-H "admin@web[1-3]:22" \[->] expands with user and port preserved
136+
.IP \[bu] 2
137+
-H "^/path/to/hostfile" \[->] read hosts from file
138+
.RE
117139

118140
.TP
119141
.BR \-C ", " \-\-cluster " " \fICLUSTER\fR
@@ -166,29 +188,47 @@ Password is never logged or printed in any output
166188

167189
.TP
168190
.BR \-f ", " \-\-filter " " \fIPATTERN\fR
169-
Filter hosts by pattern (supports wildcards like 'web*').
191+
Filter hosts by pattern. Supports both wildcards and hostlist expressions.
170192
Use with -H or -C to execute on a subset of hosts.
171-
Example: -f "web*" matches web01, web02, etc.
193+
.RS
194+
.PP
195+
Examples:
196+
.IP \[bu] 2
197+
-f "web*" \[->] matches web01, web02, etc. (glob pattern)
198+
.IP \[bu] 2
199+
-f "node[1-5]" \[->] matches node1 through node5 (hostlist expression)
200+
.IP \[bu] 2
201+
-f "node[1,3,5]" \[->] matches node1, node3, node5 (specific values)
202+
.RE
172203

173204
.TP
174205
.BR \-\-exclude " " \fIHOSTS\fR
175206
Exclude hosts from target list (comma-separated).
176-
Supports wildcard patterns: '*' (any chars), '?' (single char), '[abc]' (char set).
177-
Patterns with wildcards use glob matching; plain patterns use substring matching.
207+
Supports wildcards, glob patterns, and hostlist expressions.
178208
Applied after --filter option.
179209
.RS
180210
.PP
181-
Examples:
211+
Glob patterns:
212+
.IP \[bu] 2
213+
--exclude "db*" \[->] exclude hosts starting with 'db'
182214
.IP \[bu] 2
183-
--exclude "node2" - Exclude single host
215+
--exclude "*-backup" \[->] exclude backup nodes
184216
.IP \[bu] 2
185-
--exclude "web1,web2" - Exclude multiple hosts
217+
--exclude "web[12]" \[->] exclude web1 and web2 (glob character class)
218+
.PP
219+
Hostlist expressions:
220+
.IP \[bu] 2
221+
--exclude "node[3-5]" \[->] exclude node3, node4, node5 (range)
186222
.IP \[bu] 2
187-
--exclude "db*" - Exclude hosts starting with 'db'
223+
--exclude "node[1,3,5]" \[->] exclude node1, node3, node5 (specific values)
188224
.IP \[bu] 2
189-
--exclude "*-backup" - Exclude backup nodes
225+
--exclude "rack[1-2]-node[1-3]" \[->] exclude 6 hosts (cartesian product)
226+
.PP
227+
Simple patterns:
190228
.IP \[bu] 2
191-
--exclude "web[12]" - Exclude web1 and web2
229+
--exclude "node2" \[->] exclude single host
230+
.IP \[bu] 2
231+
--exclude "web1,web2" \[->] exclude multiple hosts
192232
.RE
193233

194234
.TP
@@ -1094,6 +1134,79 @@ Current node's role (main or sub)
10941134

10951135
Note: Backend.AI multi-node clusters use SSH port 2200 by default, which is automatically configured.
10961136

1137+
.SH HOSTLIST EXPRESSIONS
1138+
Hostlist expressions provide a compact way to specify multiple hosts using range notation,
1139+
compatible with pdsh syntax. This allows efficient targeting of large numbers of hosts
1140+
without listing each one individually.
1141+
1142+
.SS Basic Syntax
1143+
.TP
1144+
.B Simple range
1145+
.B node[1-5]
1146+
expands to node1, node2, node3, node4, node5
1147+
.TP
1148+
.B Zero-padded range
1149+
.B node[01-05]
1150+
expands to node01, node02, node03, node04, node05
1151+
.TP
1152+
.B Comma-separated values
1153+
.B node[1,3,5]
1154+
expands to node1, node3, node5
1155+
.TP
1156+
.B Mixed ranges and values
1157+
.B node[1-3,7,9-10]
1158+
expands to node1, node2, node3, node7, node9, node10
1159+
1160+
.SS Advanced Patterns
1161+
.TP
1162+
.B Multiple ranges (cartesian product)
1163+
.B rack[1-2]-node[1-3]
1164+
expands to rack1-node1, rack1-node2, rack1-node3, rack2-node1, rack2-node2, rack2-node3
1165+
.TP
1166+
.B Domain suffix
1167+
.B web[1-3].example.com
1168+
expands to web1.example.com, web2.example.com, web3.example.com
1169+
.TP
1170+
.B With user and port
1171+
.B admin@server[1-3]:2222
1172+
expands to admin@server1:2222, admin@server2:2222, admin@server3:2222
1173+
1174+
.SS File Input
1175+
.TP
1176+
.B ^/path/to/hostfile
1177+
Reads hosts from file, one per line. Lines starting with # are comments.
1178+
Maximum file size: 1MB, maximum lines: 100,000.
1179+
1180+
.SS Using with Options
1181+
Hostlist expressions can be used with:
1182+
.TP
1183+
.B -H, --hosts
1184+
Specify target hosts:
1185+
.B bssh -H "node[1-10]" "uptime"
1186+
.TP
1187+
.B --filter
1188+
Include only matching hosts:
1189+
.B bssh -c cluster --filter "web[1-5]" "systemctl status nginx"
1190+
.TP
1191+
.B --exclude
1192+
Exclude matching hosts:
1193+
.B bssh -c cluster --exclude "node[1,3,5]" "df -h"
1194+
1195+
.SS Examples
1196+
.nf
1197+
# Execute on 100 nodes
1198+
bssh -H "compute[001-100]" "hostname"
1199+
1200+
# Target specific racks
1201+
bssh -H "rack[A-C]-node[1-8]" "uptime"
1202+
1203+
# Use hosts from file
1204+
bssh -H "^/etc/cluster/hosts" "date"
1205+
1206+
# Combine with exclusions
1207+
bssh -H "node[1-20]" --exclude "node[5,10,15]" "ps aux"
1208+
.fi
1209+
10971210
.SH EXAMPLES
10981211

10991212
.SS SSH Compatibility Mode (Single Host)

0 commit comments

Comments
 (0)