bt-cli 0.4.21__py3-none-any.whl → 0.4.22__py3-none-any.whl
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.
- bt_cli/epmw/commands/quick.py +178 -0
- bt_cli/pra/commands/vault.py +32 -4
- {bt_cli-0.4.21.dist-info → bt_cli-0.4.22.dist-info}/METADATA +1 -1
- {bt_cli-0.4.21.dist-info → bt_cli-0.4.22.dist-info}/RECORD +6 -6
- {bt_cli-0.4.21.dist-info → bt_cli-0.4.22.dist-info}/WHEEL +0 -0
- {bt_cli-0.4.21.dist-info → bt_cli-0.4.22.dist-info}/entry_points.txt +0 -0
bt_cli/epmw/commands/quick.py
CHANGED
|
@@ -10,6 +10,7 @@ from rich.console import Console
|
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
12
|
from ...core.output import print_api_error, print_error, print_warning, print_success
|
|
13
|
+
from ...core.prompts import prompt_from_list
|
|
13
14
|
|
|
14
15
|
app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
|
|
15
16
|
console = Console()
|
|
@@ -346,3 +347,180 @@ def group_status(
|
|
|
346
347
|
except Exception as e:
|
|
347
348
|
print_api_error(e, "quick status")
|
|
348
349
|
raise typer.Exit(1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command("move-computer")
|
|
353
|
+
def move_computer(
|
|
354
|
+
computer: Optional[str] = typer.Option(None, "--computer", "-c", help="Computer name or ID (partial match for name)"),
|
|
355
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Target group name or ID"),
|
|
356
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
357
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Move a computer to a different group.
|
|
360
|
+
|
|
361
|
+
Interactive workflow to change a computer's group membership.
|
|
362
|
+
Lists computers (optionally filtered), then lists available groups,
|
|
363
|
+
and moves the selected computer to the selected group.
|
|
364
|
+
|
|
365
|
+
Examples:
|
|
366
|
+
bt epmw quick move-computer # Interactive selection
|
|
367
|
+
bt epmw quick move-computer -c "CorpWS01" # Filter by computer name
|
|
368
|
+
bt epmw quick move-computer -c "CorpWS01" -g "Servers" # Specify both
|
|
369
|
+
bt epmw quick move-computer -c "CorpWS01" -g "Servers" -f # Skip confirmation
|
|
370
|
+
"""
|
|
371
|
+
from ..client import get_client
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
client = get_client()
|
|
375
|
+
|
|
376
|
+
# Step 1: Get and filter computers
|
|
377
|
+
console.print("[dim]Fetching computers...[/dim]")
|
|
378
|
+
computers = client.list_computers()
|
|
379
|
+
|
|
380
|
+
if not computers:
|
|
381
|
+
print_error("No computers found")
|
|
382
|
+
raise typer.Exit(1)
|
|
383
|
+
|
|
384
|
+
# Filter by computer name/ID if provided
|
|
385
|
+
if computer:
|
|
386
|
+
computer_lower = computer.lower()
|
|
387
|
+
filtered = [
|
|
388
|
+
c for c in computers
|
|
389
|
+
if computer_lower in (c.get("host") or "").lower()
|
|
390
|
+
or computer_lower in (c.get("id") or "").lower()
|
|
391
|
+
]
|
|
392
|
+
if not filtered:
|
|
393
|
+
print_error(f"No computers matching '{computer}'")
|
|
394
|
+
raise typer.Exit(1)
|
|
395
|
+
computers = filtered
|
|
396
|
+
|
|
397
|
+
# Show computers and select one
|
|
398
|
+
if len(computers) == 1:
|
|
399
|
+
selected_computer = computers[0]
|
|
400
|
+
console.print(f"[dim]Found computer: {selected_computer.get('host')}[/dim]")
|
|
401
|
+
else:
|
|
402
|
+
# Show list and prompt for selection
|
|
403
|
+
table = Table(title="Available Computers")
|
|
404
|
+
table.add_column("#", style="dim", justify="right")
|
|
405
|
+
table.add_column("Host", style="cyan")
|
|
406
|
+
table.add_column("Domain", style="yellow")
|
|
407
|
+
table.add_column("Current Group", style="green")
|
|
408
|
+
table.add_column("Status", style="magenta")
|
|
409
|
+
|
|
410
|
+
for idx, comp in enumerate(computers, 1):
|
|
411
|
+
table.add_row(
|
|
412
|
+
str(idx),
|
|
413
|
+
comp.get("host", ""),
|
|
414
|
+
comp.get("domain", "") or "-",
|
|
415
|
+
comp.get("groupName", "") or "-",
|
|
416
|
+
comp.get("connectionStatus", ""),
|
|
417
|
+
)
|
|
418
|
+
console.print(table)
|
|
419
|
+
|
|
420
|
+
selection = typer.prompt("Select computer number", type=int)
|
|
421
|
+
if selection < 1 or selection > len(computers):
|
|
422
|
+
print_error(f"Invalid selection. Choose 1-{len(computers)}")
|
|
423
|
+
raise typer.Exit(1)
|
|
424
|
+
selected_computer = computers[selection - 1]
|
|
425
|
+
|
|
426
|
+
computer_id = selected_computer.get("id")
|
|
427
|
+
computer_host = selected_computer.get("host")
|
|
428
|
+
current_group = selected_computer.get("groupName") or "(No Group)"
|
|
429
|
+
|
|
430
|
+
# Step 2: Get and filter groups
|
|
431
|
+
console.print("[dim]Fetching groups...[/dim]")
|
|
432
|
+
groups = client.list_groups()
|
|
433
|
+
|
|
434
|
+
if not groups:
|
|
435
|
+
print_error("No groups found")
|
|
436
|
+
raise typer.Exit(1)
|
|
437
|
+
|
|
438
|
+
# Filter by group name/ID if provided
|
|
439
|
+
if group:
|
|
440
|
+
group_lower = group.lower()
|
|
441
|
+
filtered = [
|
|
442
|
+
g for g in groups
|
|
443
|
+
if group_lower in (g.get("name") or "").lower()
|
|
444
|
+
or group_lower in (g.get("id") or "").lower()
|
|
445
|
+
]
|
|
446
|
+
if not filtered:
|
|
447
|
+
print_error(f"No groups matching '{group}'")
|
|
448
|
+
raise typer.Exit(1)
|
|
449
|
+
groups = filtered
|
|
450
|
+
|
|
451
|
+
# Show groups and select one
|
|
452
|
+
if len(groups) == 1:
|
|
453
|
+
selected_group = groups[0]
|
|
454
|
+
console.print(f"[dim]Target group: {selected_group.get('name')}[/dim]")
|
|
455
|
+
else:
|
|
456
|
+
# Show list and prompt for selection
|
|
457
|
+
table = Table(title="Available Groups")
|
|
458
|
+
table.add_column("#", style="dim", justify="right")
|
|
459
|
+
table.add_column("Name", style="cyan")
|
|
460
|
+
table.add_column("Computers", style="green", justify="right")
|
|
461
|
+
table.add_column("Policy", style="yellow")
|
|
462
|
+
|
|
463
|
+
for idx, grp in enumerate(groups, 1):
|
|
464
|
+
table.add_row(
|
|
465
|
+
str(idx),
|
|
466
|
+
grp.get("name", ""),
|
|
467
|
+
str(grp.get("computerCount", 0)),
|
|
468
|
+
grp.get("policyName", "") or "-",
|
|
469
|
+
)
|
|
470
|
+
console.print(table)
|
|
471
|
+
|
|
472
|
+
selection = typer.prompt("Select target group number", type=int)
|
|
473
|
+
if selection < 1 or selection > len(groups):
|
|
474
|
+
print_error(f"Invalid selection. Choose 1-{len(groups)}")
|
|
475
|
+
raise typer.Exit(1)
|
|
476
|
+
selected_group = groups[selection - 1]
|
|
477
|
+
|
|
478
|
+
group_id = selected_group.get("id")
|
|
479
|
+
group_name = selected_group.get("name")
|
|
480
|
+
|
|
481
|
+
# Check if already in target group
|
|
482
|
+
if current_group == group_name:
|
|
483
|
+
console.print(f"[yellow]Computer '{computer_host}' is already in group '{group_name}'[/yellow]")
|
|
484
|
+
raise typer.Exit(0)
|
|
485
|
+
|
|
486
|
+
# Step 3: Confirm and move
|
|
487
|
+
if not force:
|
|
488
|
+
console.print()
|
|
489
|
+
console.print(f"[bold]Move computer:[/bold] {computer_host}")
|
|
490
|
+
console.print(f"[bold]From group:[/bold] {current_group}")
|
|
491
|
+
console.print(f"[bold]To group:[/bold] {group_name}")
|
|
492
|
+
console.print()
|
|
493
|
+
confirm = typer.confirm("Proceed with move?")
|
|
494
|
+
if not confirm:
|
|
495
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
496
|
+
raise typer.Exit(0)
|
|
497
|
+
|
|
498
|
+
# Perform the move
|
|
499
|
+
console.print("[dim]Moving computer to group...[/dim]")
|
|
500
|
+
client.assign_computers_to_group(group_id, [computer_id])
|
|
501
|
+
|
|
502
|
+
if output == "json":
|
|
503
|
+
result = {
|
|
504
|
+
"computer": {
|
|
505
|
+
"id": computer_id,
|
|
506
|
+
"host": computer_host,
|
|
507
|
+
},
|
|
508
|
+
"previousGroup": current_group,
|
|
509
|
+
"newGroup": group_name,
|
|
510
|
+
"success": True,
|
|
511
|
+
}
|
|
512
|
+
console.print_json(json.dumps(result, default=str))
|
|
513
|
+
else:
|
|
514
|
+
print_success(f"Moved '{computer_host}' from '{current_group}' to '{group_name}'")
|
|
515
|
+
|
|
516
|
+
except httpx.HTTPStatusError as e:
|
|
517
|
+
print_api_error(e, "quick move-computer")
|
|
518
|
+
raise typer.Exit(1)
|
|
519
|
+
except httpx.RequestError as e:
|
|
520
|
+
print_api_error(e, "quick move-computer")
|
|
521
|
+
raise typer.Exit(1)
|
|
522
|
+
except typer.Exit:
|
|
523
|
+
raise
|
|
524
|
+
except Exception as e:
|
|
525
|
+
print_api_error(e, "quick move-computer")
|
|
526
|
+
raise typer.Exit(1)
|
bt_cli/pra/commands/vault.py
CHANGED
|
@@ -137,13 +137,16 @@ def create_vault_account(
|
|
|
137
137
|
name: str = typer.Option(..., "--name", "-n", help="Account name"),
|
|
138
138
|
account_type: str = typer.Option(
|
|
139
139
|
..., "--type", "-t",
|
|
140
|
-
help="Account type: username_password, ssh, or
|
|
140
|
+
help="Account type: username_password, ssh, ssh_ca, or token"
|
|
141
141
|
),
|
|
142
142
|
username: Optional[str] = typer.Option(None, "--username", "-u", help="Username (for username_password)"),
|
|
143
143
|
password: Optional[str] = typer.Option(None, "--password", "-p", help="Password (for username_password)"),
|
|
144
|
+
token: Optional[str] = typer.Option(None, "--token", help="Token value (for token type)"),
|
|
145
|
+
token_file: Optional[str] = typer.Option(None, "--token-file", help="Path to file containing token"),
|
|
144
146
|
private_key: Optional[str] = typer.Option(None, "--private-key", help="SSH private key (for ssh type)"),
|
|
145
147
|
private_key_file: Optional[str] = typer.Option(None, "--private-key-file", help="Path to SSH private key file"),
|
|
146
148
|
description: Optional[str] = typer.Option(None, "--description", "-d", help="Account description"),
|
|
149
|
+
account_group_id: Optional[int] = typer.Option(None, "--group-id", "-g", help="Vault Account Group ID"),
|
|
147
150
|
personal: bool = typer.Option(False, "--personal", help="Mark as personal account"),
|
|
148
151
|
output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
|
|
149
152
|
):
|
|
@@ -158,20 +161,29 @@ def create_vault_account(
|
|
|
158
161
|
|
|
159
162
|
# Create SSH CA account (for certificate-based auth)
|
|
160
163
|
bt pra vault accounts create -n "ssh-ca" -t ssh_ca --private-key-file /path/to/ca_key
|
|
164
|
+
|
|
165
|
+
# Create token account (API keys, bearer tokens, etc.)
|
|
166
|
+
bt pra vault accounts create -n "api-key" -t token --token "sk-abc123..."
|
|
167
|
+
|
|
168
|
+
# Create token account from file
|
|
169
|
+
bt pra vault accounts create -n "service-token" -t token --token-file /path/to/token.txt
|
|
161
170
|
"""
|
|
162
171
|
from bt_cli.pra.client import get_client
|
|
163
172
|
from pathlib import Path
|
|
164
173
|
|
|
165
|
-
# Validate account type
|
|
166
|
-
valid_types = ["username_password", "ssh", "ssh_ca"]
|
|
174
|
+
# Validate account type (token maps to opaque_token in API)
|
|
175
|
+
valid_types = ["username_password", "ssh", "ssh_ca", "token"]
|
|
167
176
|
if account_type not in valid_types:
|
|
168
177
|
print_error(f"Invalid account type '{account_type}'. Must be one of: {', '.join(valid_types)}")
|
|
169
178
|
raise typer.Exit(1)
|
|
170
179
|
|
|
180
|
+
# Map 'token' to API type 'opaque_token'
|
|
181
|
+
api_type = "opaque_token" if account_type == "token" else account_type
|
|
182
|
+
|
|
171
183
|
# Build account data
|
|
172
184
|
data = {
|
|
173
185
|
"name": name,
|
|
174
|
-
"type":
|
|
186
|
+
"type": api_type,
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
if username:
|
|
@@ -180,9 +192,21 @@ def create_vault_account(
|
|
|
180
192
|
data["password"] = password
|
|
181
193
|
if description:
|
|
182
194
|
data["description"] = description
|
|
195
|
+
if account_group_id:
|
|
196
|
+
data["account_group_id"] = account_group_id
|
|
183
197
|
if personal:
|
|
184
198
|
data["personal"] = True
|
|
185
199
|
|
|
200
|
+
# Handle token (from file or direct)
|
|
201
|
+
if token_file:
|
|
202
|
+
token_path = Path(token_file).expanduser()
|
|
203
|
+
if not token_path.exists():
|
|
204
|
+
print_error(f"Token file not found: {token_file}")
|
|
205
|
+
raise typer.Exit(1)
|
|
206
|
+
data["token"] = token_path.read_text().strip()
|
|
207
|
+
elif token:
|
|
208
|
+
data["token"] = token
|
|
209
|
+
|
|
186
210
|
# Handle private key (from file or direct)
|
|
187
211
|
if private_key_file:
|
|
188
212
|
key_path = Path(private_key_file).expanduser()
|
|
@@ -202,6 +226,10 @@ def create_vault_account(
|
|
|
202
226
|
if not data.get("private_key"):
|
|
203
227
|
print_error(f"Private key is required for {account_type} type (use --private-key or --private-key-file)")
|
|
204
228
|
raise typer.Exit(1)
|
|
229
|
+
elif account_type == "token":
|
|
230
|
+
if not data.get("token"):
|
|
231
|
+
print_error("Token is required for token type (use --token or --token-file)")
|
|
232
|
+
raise typer.Exit(1)
|
|
205
233
|
|
|
206
234
|
try:
|
|
207
235
|
client = get_client()
|
|
@@ -56,7 +56,7 @@ bt_cli/epmw/commands/computers.py,sha256=K0eBd8HOaVCe8hhHPlUuqCrZduhPuvwlkxdYt6Y
|
|
|
56
56
|
bt_cli/epmw/commands/events.py,sha256=not-9_poxQWRBB4sqZSzJo6031A3IZTdg6uAl2Agk2g,7707
|
|
57
57
|
bt_cli/epmw/commands/groups.py,sha256=PaMcjqfr2b3xS3W3V2A9CFTlMOAs38UmJbi5LKXbbg0,6759
|
|
58
58
|
bt_cli/epmw/commands/policies.py,sha256=0jFQ-ZLzTAC6rPbMWL3DfWm0jdgvE2iwR_dHUHI-QG4,24600
|
|
59
|
-
bt_cli/epmw/commands/quick.py,sha256=
|
|
59
|
+
bt_cli/epmw/commands/quick.py,sha256=3jE1Q2tPFNhKZlLXA670Ay0BaZOxq32hhqxc4-5bnVA,20967
|
|
60
60
|
bt_cli/epmw/commands/requests.py,sha256=qcx6MsPXVm2UkaBIkhpf3RZh6Lo-BBz2OPF4Tl3h4p8,8251
|
|
61
61
|
bt_cli/epmw/commands/roles.py,sha256=WYAelLUwzJkBB7rlKoR4FqSxfdoDjV310KcNl6ixCCM,2396
|
|
62
62
|
bt_cli/epmw/commands/tasks.py,sha256=5YFg5BsfEnN1L4KkPrIwbaUy4a2XJUgUpo1kQDji_EI,1033
|
|
@@ -76,7 +76,7 @@ bt_cli/pra/commands/policies.py,sha256=5ekjEGKaIEdjpuUYyuBUvNdJgH3GKQkajRep0_i8Q
|
|
|
76
76
|
bt_cli/pra/commands/quick.py,sha256=W0JrS5XYMfl5Jl0PjroeAbFGWszPsu-NFH0_REy6Qkk,18892
|
|
77
77
|
bt_cli/pra/commands/teams.py,sha256=tr_stOR6CkvcZ4jDFg6gUbPZ-d8dvnN_wEgdA_hTepc,2346
|
|
78
78
|
bt_cli/pra/commands/users.py,sha256=Ui9ByM2CIaI8hN4N9zkTD3Rw2gIU0jlHVWKpZEnaR3c,2733
|
|
79
|
-
bt_cli/pra/commands/vault.py,sha256=
|
|
79
|
+
bt_cli/pra/commands/vault.py,sha256=xH9HIer-X4lrTJblEvKB3z0QgvhGkqa_ANYgOjSHoNo,21391
|
|
80
80
|
bt_cli/pra/models/__init__.py,sha256=5S6ogGJDZY1O8gCFbCCQ5vN61wocP5J-EO2s55ebV2Y,614
|
|
81
81
|
bt_cli/pra/models/common.py,sha256=hnQOndbt38YiLuS2kZxbpK4QsX1ONyKtOgE6RcWFjVU,247
|
|
82
82
|
bt_cli/pra/models/jump_client.py,sha256=M3-4U66rcPzrfjAUFzQ0xxgWTPuBg5DdLCwJrgP9O3k,684
|
|
@@ -115,7 +115,7 @@ bt_cli/pws/models/account.py,sha256=OSCMyULPOH1Yu2WOzK0ZQhSRrggGpb2JPHScwGLqUgI,
|
|
|
115
115
|
bt_cli/pws/models/asset.py,sha256=Fl0AlR4_9Yyyu36FL1eKF29DNsxsB-r7FaOBRlfOg2Q,4081
|
|
116
116
|
bt_cli/pws/models/common.py,sha256=D9Ah4ob5CIiFhTt_IR9nF2cBWRHS2z9OyBR2Sss5yzw,3487
|
|
117
117
|
bt_cli/pws/models/system.py,sha256=D_J0x1A92H2n6BsaBEK9PSAAcs3BTifA5-M9SQqQFGA,5856
|
|
118
|
-
bt_cli-0.4.
|
|
119
|
-
bt_cli-0.4.
|
|
120
|
-
bt_cli-0.4.
|
|
121
|
-
bt_cli-0.4.
|
|
118
|
+
bt_cli-0.4.22.dist-info/METADATA,sha256=vissSGp_CPrsPrqJ2WoTTTK-LsZNVRZq34hWCTlsXUk,11862
|
|
119
|
+
bt_cli-0.4.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
120
|
+
bt_cli-0.4.22.dist-info/entry_points.txt,sha256=NCOEqTI-XKpJOux0JKKhbRElz0B7upayh_d99X5hoLs,38
|
|
121
|
+
bt_cli-0.4.22.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|