bt-cli 0.4.13__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.
Files changed (121) hide show
  1. bt_cli/__init__.py +3 -0
  2. bt_cli/cli.py +830 -0
  3. bt_cli/commands/__init__.py +1 -0
  4. bt_cli/commands/configure.py +415 -0
  5. bt_cli/commands/learn.py +229 -0
  6. bt_cli/commands/quick.py +784 -0
  7. bt_cli/core/__init__.py +1 -0
  8. bt_cli/core/auth.py +213 -0
  9. bt_cli/core/client.py +313 -0
  10. bt_cli/core/config.py +393 -0
  11. bt_cli/core/config_file.py +420 -0
  12. bt_cli/core/csv_utils.py +91 -0
  13. bt_cli/core/errors.py +247 -0
  14. bt_cli/core/output.py +205 -0
  15. bt_cli/core/prompts.py +87 -0
  16. bt_cli/core/rest_debug.py +221 -0
  17. bt_cli/data/CLAUDE.md +94 -0
  18. bt_cli/data/__init__.py +0 -0
  19. bt_cli/data/skills/bt/SKILL.md +108 -0
  20. bt_cli/data/skills/entitle/SKILL.md +170 -0
  21. bt_cli/data/skills/epmw/SKILL.md +144 -0
  22. bt_cli/data/skills/pra/SKILL.md +150 -0
  23. bt_cli/data/skills/pws/SKILL.md +198 -0
  24. bt_cli/entitle/__init__.py +1 -0
  25. bt_cli/entitle/client/__init__.py +5 -0
  26. bt_cli/entitle/client/base.py +443 -0
  27. bt_cli/entitle/commands/__init__.py +24 -0
  28. bt_cli/entitle/commands/accounts.py +53 -0
  29. bt_cli/entitle/commands/applications.py +39 -0
  30. bt_cli/entitle/commands/auth.py +68 -0
  31. bt_cli/entitle/commands/bundles.py +218 -0
  32. bt_cli/entitle/commands/integrations.py +60 -0
  33. bt_cli/entitle/commands/permissions.py +70 -0
  34. bt_cli/entitle/commands/policies.py +97 -0
  35. bt_cli/entitle/commands/resources.py +131 -0
  36. bt_cli/entitle/commands/roles.py +74 -0
  37. bt_cli/entitle/commands/users.py +123 -0
  38. bt_cli/entitle/commands/workflows.py +187 -0
  39. bt_cli/entitle/models/__init__.py +31 -0
  40. bt_cli/entitle/models/bundle.py +28 -0
  41. bt_cli/entitle/models/common.py +37 -0
  42. bt_cli/entitle/models/integration.py +30 -0
  43. bt_cli/entitle/models/permission.py +27 -0
  44. bt_cli/entitle/models/policy.py +25 -0
  45. bt_cli/entitle/models/resource.py +29 -0
  46. bt_cli/entitle/models/role.py +28 -0
  47. bt_cli/entitle/models/user.py +24 -0
  48. bt_cli/entitle/models/workflow.py +55 -0
  49. bt_cli/epmw/__init__.py +1 -0
  50. bt_cli/epmw/client/__init__.py +5 -0
  51. bt_cli/epmw/client/base.py +848 -0
  52. bt_cli/epmw/commands/__init__.py +33 -0
  53. bt_cli/epmw/commands/audits.py +250 -0
  54. bt_cli/epmw/commands/auth.py +55 -0
  55. bt_cli/epmw/commands/computers.py +140 -0
  56. bt_cli/epmw/commands/events.py +233 -0
  57. bt_cli/epmw/commands/groups.py +215 -0
  58. bt_cli/epmw/commands/policies.py +673 -0
  59. bt_cli/epmw/commands/quick.py +348 -0
  60. bt_cli/epmw/commands/requests.py +224 -0
  61. bt_cli/epmw/commands/roles.py +78 -0
  62. bt_cli/epmw/commands/tasks.py +38 -0
  63. bt_cli/epmw/commands/users.py +219 -0
  64. bt_cli/epmw/models/__init__.py +1 -0
  65. bt_cli/pra/__init__.py +1 -0
  66. bt_cli/pra/client/__init__.py +5 -0
  67. bt_cli/pra/client/base.py +618 -0
  68. bt_cli/pra/commands/__init__.py +30 -0
  69. bt_cli/pra/commands/auth.py +55 -0
  70. bt_cli/pra/commands/import_export.py +442 -0
  71. bt_cli/pra/commands/jump_clients.py +139 -0
  72. bt_cli/pra/commands/jump_groups.py +146 -0
  73. bt_cli/pra/commands/jump_items.py +638 -0
  74. bt_cli/pra/commands/jumpoints.py +95 -0
  75. bt_cli/pra/commands/policies.py +197 -0
  76. bt_cli/pra/commands/quick.py +470 -0
  77. bt_cli/pra/commands/teams.py +81 -0
  78. bt_cli/pra/commands/users.py +87 -0
  79. bt_cli/pra/commands/vault.py +564 -0
  80. bt_cli/pra/models/__init__.py +27 -0
  81. bt_cli/pra/models/common.py +12 -0
  82. bt_cli/pra/models/jump_client.py +25 -0
  83. bt_cli/pra/models/jump_group.py +15 -0
  84. bt_cli/pra/models/jump_item.py +72 -0
  85. bt_cli/pra/models/jumpoint.py +19 -0
  86. bt_cli/pra/models/team.py +14 -0
  87. bt_cli/pra/models/user.py +17 -0
  88. bt_cli/pra/models/vault.py +45 -0
  89. bt_cli/pws/__init__.py +1 -0
  90. bt_cli/pws/client/__init__.py +5 -0
  91. bt_cli/pws/client/base.py +356 -0
  92. bt_cli/pws/client/beyondinsight.py +869 -0
  93. bt_cli/pws/client/passwordsafe.py +1786 -0
  94. bt_cli/pws/commands/__init__.py +33 -0
  95. bt_cli/pws/commands/accounts.py +372 -0
  96. bt_cli/pws/commands/assets.py +311 -0
  97. bt_cli/pws/commands/auth.py +166 -0
  98. bt_cli/pws/commands/clouds.py +221 -0
  99. bt_cli/pws/commands/config.py +344 -0
  100. bt_cli/pws/commands/credentials.py +347 -0
  101. bt_cli/pws/commands/databases.py +306 -0
  102. bt_cli/pws/commands/directories.py +199 -0
  103. bt_cli/pws/commands/functional.py +298 -0
  104. bt_cli/pws/commands/import_export.py +452 -0
  105. bt_cli/pws/commands/platforms.py +118 -0
  106. bt_cli/pws/commands/quick.py +1646 -0
  107. bt_cli/pws/commands/search.py +256 -0
  108. bt_cli/pws/commands/secrets.py +1343 -0
  109. bt_cli/pws/commands/systems.py +389 -0
  110. bt_cli/pws/commands/users.py +415 -0
  111. bt_cli/pws/commands/workgroups.py +166 -0
  112. bt_cli/pws/config.py +18 -0
  113. bt_cli/pws/models/__init__.py +19 -0
  114. bt_cli/pws/models/account.py +186 -0
  115. bt_cli/pws/models/asset.py +102 -0
  116. bt_cli/pws/models/common.py +132 -0
  117. bt_cli/pws/models/system.py +121 -0
  118. bt_cli-0.4.13.dist-info/METADATA +417 -0
  119. bt_cli-0.4.13.dist-info/RECORD +121 -0
  120. bt_cli-0.4.13.dist-info/WHEEL +4 -0
  121. bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,442 @@
1
+ """Import/export commands for PRA bulk operations."""
2
+
3
+ from typing import Optional
4
+ import json
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ...core.output import print_api_error, print_error, print_success, print_warning
12
+ from ...core.csv_utils import read_csv, write_csv, validate_required_fields, parse_bool, parse_int
13
+ from ..client import get_client
14
+
15
+ import_app = typer.Typer(no_args_is_help=True, help="Import resources from CSV files")
16
+ export_app = typer.Typer(no_args_is_help=True, help="Export sample CSV templates")
17
+ console = Console()
18
+
19
+ # Column definitions for CSV formats
20
+ JUMP_ITEMS_COLUMNS = [
21
+ "type", "name", "hostname", "jumpoint_id", "jump_group_id",
22
+ "username", "port", "protocol", "domain", "tag"
23
+ ]
24
+
25
+ VAULT_ACCOUNTS_COLUMNS = [
26
+ "name", "type", "username", "password", "description", "private_key"
27
+ ]
28
+
29
+ # Sample data for templates
30
+ JUMP_ITEMS_SAMPLE = [
31
+ {
32
+ "type": "shell", "name": "web-server-01", "hostname": "10.0.1.50",
33
+ "jumpoint_id": "3", "jump_group_id": "24",
34
+ "username": "ec2-admin", "port": "22", "protocol": "ssh",
35
+ "domain": "", "tag": "linux"
36
+ },
37
+ {
38
+ "type": "shell", "name": "web-server-02", "hostname": "10.0.1.51",
39
+ "jumpoint_id": "3", "jump_group_id": "24",
40
+ "username": "ec2-admin", "port": "22", "protocol": "ssh",
41
+ "domain": "", "tag": "linux"
42
+ },
43
+ {
44
+ "type": "shell", "name": "db-server-01", "hostname": "10.0.1.60",
45
+ "jumpoint_id": "3", "jump_group_id": "24",
46
+ "username": "ec2-admin", "port": "22", "protocol": "ssh",
47
+ "domain": "", "tag": "database"
48
+ },
49
+ {
50
+ "type": "rdp", "name": "win-server-01", "hostname": "10.0.2.10",
51
+ "jumpoint_id": "3", "jump_group_id": "31",
52
+ "username": "", "port": "3389", "protocol": "",
53
+ "domain": "nexusdyn.corp", "tag": "windows"
54
+ },
55
+ {
56
+ "type": "rdp", "name": "win-server-02", "hostname": "10.0.2.11",
57
+ "jumpoint_id": "3", "jump_group_id": "31",
58
+ "username": "", "port": "3389", "protocol": "",
59
+ "domain": "nexusdyn.corp", "tag": "windows"
60
+ },
61
+ ]
62
+
63
+ VAULT_ACCOUNTS_SAMPLE = [
64
+ {
65
+ "name": "server-admin", "type": "username_password",
66
+ "username": "admin@corp.local", "password": "ServerAdmin#2026!",
67
+ "description": "Domain admin", "private_key": ""
68
+ },
69
+ {
70
+ "name": "postgres-prod", "type": "username_password",
71
+ "username": "postgres", "password": "PgProd#2026!",
72
+ "description": "Production DB", "private_key": ""
73
+ },
74
+ {
75
+ "name": "ssh-deploy-key", "type": "ssh",
76
+ "username": "deploy", "password": "",
77
+ "description": "Deployment SSH key", "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n..."
78
+ },
79
+ ]
80
+
81
+
82
+ @import_app.command("jump-items")
83
+ def import_jump_items(
84
+ file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
85
+ dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
86
+ jumpoint: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Override jumpoint ID for all rows"),
87
+ jump_group: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Override jump group ID for all rows"),
88
+ ) -> None:
89
+ """Import jump items (shell and RDP) from CSV.
90
+
91
+ The 'type' column determines whether to create a shell or RDP jump item.
92
+
93
+ Required columns: type, name, hostname, jumpoint_id, jump_group_id
94
+ Optional: username, port, protocol, domain, tag
95
+
96
+ Examples:
97
+ bt pra import jump-items --file jump-items.csv --dry-run
98
+ bt pra import jump-items --file jump-items.csv
99
+ bt pra import jump-items --file jump-items.csv --jumpoint 3 --jump-group 24
100
+ """
101
+ try:
102
+ rows = read_csv(file)
103
+ if not rows:
104
+ print_error("CSV file is empty")
105
+ raise typer.Exit(1)
106
+
107
+ console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
108
+
109
+ # Validate all rows first
110
+ errors = []
111
+ required = ["type", "name", "hostname"]
112
+ if not jumpoint:
113
+ required.append("jumpoint_id")
114
+ if not jump_group:
115
+ required.append("jump_group_id")
116
+
117
+ for i, row in enumerate(rows, 1):
118
+ row_errors = validate_required_fields(row, required, i)
119
+ errors.extend(row_errors)
120
+
121
+ # Validate type
122
+ item_type = row.get("type", "").strip().lower()
123
+ if item_type and item_type not in ("shell", "rdp"):
124
+ errors.append(f"Row {i}: Invalid type '{item_type}' (must be 'shell' or 'rdp')")
125
+
126
+ if errors:
127
+ print_error("Validation errors:")
128
+ for err in errors[:20]:
129
+ console.print(f" [red]{err}[/red]")
130
+ if len(errors) > 20:
131
+ console.print(f" [red]... and {len(errors) - 20} more errors[/red]")
132
+ raise typer.Exit(1)
133
+
134
+ # Count by type
135
+ shell_count = sum(1 for r in rows if r.get("type", "").lower() == "shell")
136
+ rdp_count = sum(1 for r in rows if r.get("type", "").lower() == "rdp")
137
+ console.print(f"[dim]Found {shell_count} shell and {rdp_count} RDP jump items[/dim]")
138
+
139
+ if dry_run:
140
+ console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
141
+ table = Table(title="Jump Items to Create")
142
+ table.add_column("Type", style="magenta")
143
+ table.add_column("Name", style="green")
144
+ table.add_column("Hostname", style="cyan")
145
+ table.add_column("Jumpoint", style="yellow")
146
+ table.add_column("Group", style="blue")
147
+ table.add_column("Username", style="dim")
148
+
149
+ for row in rows:
150
+ jp = jumpoint or parse_int(row.get("jumpoint_id", ""))
151
+ jg = jump_group or parse_int(row.get("jump_group_id", ""))
152
+ table.add_row(
153
+ row.get("type", "").upper(),
154
+ row.get("name", ""),
155
+ row.get("hostname", ""),
156
+ str(jp),
157
+ str(jg),
158
+ row.get("username", "") or "-"
159
+ )
160
+
161
+ console.print(table)
162
+ console.print(f"\n[dim]Would create {len(rows)} jump items[/dim]")
163
+ return
164
+
165
+ # Actually import
166
+ client = get_client()
167
+ created = 0
168
+
169
+ for row in rows:
170
+ item_type = row.get("type", "").strip().lower()
171
+ name = row.get("name", "").strip()
172
+ hostname = row.get("hostname", "").strip()
173
+ jp_id = jumpoint or parse_int(row.get("jumpoint_id", ""))
174
+ jg_id = jump_group or parse_int(row.get("jump_group_id", ""))
175
+
176
+ if not jp_id or not jg_id:
177
+ print_warning(f"Skipping {name}: Missing jumpoint or jump group ID")
178
+ continue
179
+
180
+ try:
181
+ if item_type == "shell":
182
+ port = parse_int(row.get("port", ""), 22)
183
+ protocol = row.get("protocol", "").strip() or "ssh"
184
+ username = row.get("username", "").strip() or None
185
+ tag = row.get("tag", "").strip() or None
186
+
187
+ console.print(f"[dim]Creating shell jump item '{name}'...[/dim]")
188
+ result = client.create_shell_jump(
189
+ name=name,
190
+ hostname=hostname,
191
+ jumpoint_id=jp_id,
192
+ jump_group_id=jg_id,
193
+ port=port,
194
+ protocol=protocol,
195
+ username=username,
196
+ tag=tag,
197
+ )
198
+ item_id = result.get("id")
199
+ created += 1
200
+ console.print(f" [green]Created shell jump: {name} (ID: {item_id})[/green]")
201
+
202
+ elif item_type == "rdp":
203
+ port = parse_int(row.get("port", ""), 3389)
204
+ domain = row.get("domain", "").strip() or None
205
+ tag = row.get("tag", "").strip() or None
206
+
207
+ console.print(f"[dim]Creating RDP jump item '{name}'...[/dim]")
208
+ result = client.create_rdp_jump(
209
+ name=name,
210
+ hostname=hostname,
211
+ jumpoint_id=jp_id,
212
+ jump_group_id=jg_id,
213
+ rdp_port=port,
214
+ domain=domain,
215
+ tag=tag,
216
+ )
217
+ item_id = result.get("id")
218
+ created += 1
219
+ console.print(f" [green]Created RDP jump: {name} (ID: {item_id})[/green]")
220
+
221
+ except httpx.HTTPStatusError as e:
222
+ print_warning(f"Error creating {name}: {e.response.text}")
223
+ except Exception as e:
224
+ print_warning(f"Error creating {name}: {e}")
225
+
226
+ print_success(f"Import complete: {created} jump items created")
227
+
228
+ except FileNotFoundError as e:
229
+ print_error(str(e))
230
+ raise typer.Exit(1)
231
+ except typer.Exit:
232
+ raise
233
+ except Exception as e:
234
+ print_api_error(e, "import jump-items")
235
+ raise typer.Exit(1)
236
+
237
+
238
+ @import_app.command("vault-accounts")
239
+ def import_vault_accounts(
240
+ file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
241
+ dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
242
+ ) -> None:
243
+ """Import vault accounts from CSV.
244
+
245
+ Required columns: name, type (username_password|ssh|ssh_ca)
246
+ Optional: username, password, description, private_key
247
+
248
+ Examples:
249
+ bt pra import vault-accounts --file vault-accounts.csv --dry-run
250
+ bt pra import vault-accounts --file vault-accounts.csv
251
+ """
252
+ try:
253
+ rows = read_csv(file)
254
+ if not rows:
255
+ print_error("CSV file is empty")
256
+ raise typer.Exit(1)
257
+
258
+ console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
259
+
260
+ # Validate
261
+ errors = []
262
+ required = ["name", "type"]
263
+ valid_types = ("username_password", "ssh", "ssh_ca")
264
+
265
+ for i, row in enumerate(rows, 1):
266
+ row_errors = validate_required_fields(row, required, i)
267
+ errors.extend(row_errors)
268
+
269
+ # Validate type
270
+ acct_type = row.get("type", "").strip().lower()
271
+ if acct_type and acct_type not in valid_types:
272
+ errors.append(f"Row {i}: Invalid type '{acct_type}' (must be one of: {', '.join(valid_types)})")
273
+
274
+ if errors:
275
+ print_error("Validation errors:")
276
+ for err in errors[:20]:
277
+ console.print(f" [red]{err}[/red]")
278
+ raise typer.Exit(1)
279
+
280
+ if dry_run:
281
+ console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
282
+ table = Table(title="Vault Accounts to Create")
283
+ table.add_column("Name", style="green")
284
+ table.add_column("Type", style="magenta")
285
+ table.add_column("Username", style="cyan")
286
+ table.add_column("Description", style="dim")
287
+
288
+ for row in rows:
289
+ table.add_row(
290
+ row.get("name", ""),
291
+ row.get("type", ""),
292
+ row.get("username", "") or "-",
293
+ (row.get("description", "") or "-")[:40]
294
+ )
295
+
296
+ console.print(table)
297
+ console.print(f"\n[dim]Would create {len(rows)} vault accounts[/dim]")
298
+ return
299
+
300
+ # Actually import
301
+ client = get_client()
302
+ created = 0
303
+
304
+ for row in rows:
305
+ name = row.get("name", "").strip()
306
+ acct_type = row.get("type", "").strip().lower()
307
+
308
+ try:
309
+ console.print(f"[dim]Creating vault account '{name}'...[/dim]")
310
+ result = client.create_vault_account(
311
+ name=name,
312
+ account_type=acct_type,
313
+ username=row.get("username", "").strip() or None,
314
+ password=row.get("password", "").strip() or None,
315
+ description=row.get("description", "").strip() or None,
316
+ private_key=row.get("private_key", "").strip() or None,
317
+ )
318
+ acct_id = result.get("id")
319
+ created += 1
320
+ console.print(f" [green]Created vault account: {name} (ID: {acct_id})[/green]")
321
+
322
+ except httpx.HTTPStatusError as e:
323
+ print_warning(f"Error creating {name}: {e.response.text}")
324
+ except Exception as e:
325
+ print_warning(f"Error creating {name}: {e}")
326
+
327
+ print_success(f"Import complete: {created} vault accounts created")
328
+
329
+ except FileNotFoundError as e:
330
+ print_error(str(e))
331
+ raise typer.Exit(1)
332
+ except typer.Exit:
333
+ raise
334
+ except Exception as e:
335
+ print_api_error(e, "import vault-accounts")
336
+ raise typer.Exit(1)
337
+
338
+
339
+ @export_app.command("jump-items")
340
+ def export_jump_items(
341
+ file: str = typer.Option("pra-jump-items-template.csv", "--file", "-f", help="Output file path"),
342
+ sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
343
+ ) -> None:
344
+ """Export sample jump items CSV template.
345
+
346
+ Examples:
347
+ bt pra export jump-items --file jump-items-template.csv
348
+ bt pra export jump-items --file jump-items-template.csv --no-sample
349
+ """
350
+ try:
351
+ if sample:
352
+ write_csv(file, JUMP_ITEMS_SAMPLE, JUMP_ITEMS_COLUMNS)
353
+ print_success(f"Sample jump items template exported to: {file}")
354
+ console.print(f"[dim]Contains {len(JUMP_ITEMS_SAMPLE)} example rows[/dim]")
355
+ else:
356
+ # Export actual data from API
357
+ client = get_client()
358
+
359
+ rows = []
360
+
361
+ # Get shell jump items
362
+ shell_items = client.list_shell_jumps()
363
+ for item in shell_items:
364
+ rows.append({
365
+ "type": "shell",
366
+ "name": item.get("name", ""),
367
+ "hostname": item.get("hostname", ""),
368
+ "jumpoint_id": str(item.get("jumpoint_id", "")),
369
+ "jump_group_id": str(item.get("jump_group_id", "")),
370
+ "username": item.get("username", "") or "",
371
+ "port": str(item.get("port", "")),
372
+ "protocol": item.get("protocol", ""),
373
+ "domain": "",
374
+ "tag": item.get("tag", "") or "",
375
+ })
376
+
377
+ # Get RDP jump items
378
+ rdp_items = client.list_rdp_jumps()
379
+ for item in rdp_items:
380
+ rows.append({
381
+ "type": "rdp",
382
+ "name": item.get("name", ""),
383
+ "hostname": item.get("hostname", ""),
384
+ "jumpoint_id": str(item.get("jumpoint_id", "")),
385
+ "jump_group_id": str(item.get("jump_group_id", "")),
386
+ "username": "",
387
+ "port": str(item.get("rdp_port", item.get("port", ""))),
388
+ "protocol": "",
389
+ "domain": item.get("domain", "") or "",
390
+ "tag": item.get("tag", "") or "",
391
+ })
392
+
393
+ write_csv(file, rows, JUMP_ITEMS_COLUMNS)
394
+ print_success(f"Exported {len(rows)} jump items to: {file}")
395
+
396
+ except typer.Exit:
397
+ raise
398
+ except Exception as e:
399
+ print_api_error(e, "export jump-items")
400
+ raise typer.Exit(1)
401
+
402
+
403
+ @export_app.command("vault-accounts")
404
+ def export_vault_accounts(
405
+ file: str = typer.Option("pra-vault-accounts-template.csv", "--file", "-f", help="Output file path"),
406
+ sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
407
+ ) -> None:
408
+ """Export sample vault accounts CSV template.
409
+
410
+ Examples:
411
+ bt pra export vault-accounts --file vault-accounts-template.csv
412
+ bt pra export vault-accounts --file vault-accounts-template.csv --no-sample
413
+ """
414
+ try:
415
+ if sample:
416
+ write_csv(file, VAULT_ACCOUNTS_SAMPLE, VAULT_ACCOUNTS_COLUMNS)
417
+ print_success(f"Sample vault accounts template exported to: {file}")
418
+ console.print(f"[dim]Contains {len(VAULT_ACCOUNTS_SAMPLE)} example rows[/dim]")
419
+ else:
420
+ # Export actual data from API
421
+ client = get_client()
422
+
423
+ accounts = client.list_vault_accounts()
424
+ rows = []
425
+ for acct in accounts:
426
+ rows.append({
427
+ "name": acct.get("name", ""),
428
+ "type": acct.get("type", ""),
429
+ "username": acct.get("username", "") or "",
430
+ "password": "", # Don't export passwords
431
+ "description": acct.get("description", "") or "",
432
+ "private_key": "", # Don't export private keys
433
+ })
434
+
435
+ write_csv(file, rows, VAULT_ACCOUNTS_COLUMNS)
436
+ print_success(f"Exported {len(rows)} vault accounts to: {file}")
437
+
438
+ except typer.Exit:
439
+ raise
440
+ except Exception as e:
441
+ print_api_error(e, "export vault-accounts")
442
+ raise typer.Exit(1)
@@ -0,0 +1,139 @@
1
+ """Jump Client commands."""
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_success, print_api_error
9
+
10
+ app = typer.Typer(no_args_is_help=True)
11
+
12
+
13
+ @app.command("list")
14
+ def list_jump_clients(
15
+ jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group ID"),
16
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name"),
17
+ hostname: Optional[str] = typer.Option(None, "--hostname", "-h", help="Filter by hostname"),
18
+ output: OutputFormat = typer.Option(
19
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
20
+ ),
21
+ ):
22
+ """List Jump Clients (agents)."""
23
+ from bt_cli.pra.client import get_client
24
+
25
+ try:
26
+ client = get_client()
27
+ clients = client.list_jump_clients(
28
+ jump_group_id=jump_group_id,
29
+ name=name,
30
+ hostname=hostname,
31
+ )
32
+
33
+ if output == OutputFormat.JSON:
34
+ print_json(clients)
35
+ else:
36
+ columns = [
37
+ ("ID", "id"),
38
+ ("Name", "name"),
39
+ ("Hostname", "hostname"),
40
+ ("Private IP", "private_ip"),
41
+ ("Public IP", "public_ip"),
42
+ ("Connection", "connection_type"),
43
+ ("Jump Group", "jump_group_id"),
44
+ ]
45
+ print_table(clients, columns, title="Jump Clients")
46
+ except httpx.HTTPStatusError as e:
47
+ print_api_error(e, "list jump clients")
48
+ raise typer.Exit(1)
49
+ except httpx.RequestError as e:
50
+ print_api_error(e, "list jump clients")
51
+ raise typer.Exit(1)
52
+ except Exception as e:
53
+ print_api_error(e, "list jump clients")
54
+ raise typer.Exit(1)
55
+
56
+
57
+ @app.command("get")
58
+ def get_jump_client(
59
+ client_id: int = typer.Argument(..., help="Jump Client ID"),
60
+ output: OutputFormat = typer.Option(
61
+ OutputFormat.TABLE, "--output", "-o", help="Output format"
62
+ ),
63
+ ):
64
+ """Get Jump Client details."""
65
+ from bt_cli.pra.client import get_client
66
+ from rich.console import Console
67
+ from rich.panel import Panel
68
+
69
+ console = Console()
70
+
71
+ try:
72
+ client = get_client()
73
+ jc = client.get_jump_client(client_id)
74
+
75
+ if output == OutputFormat.JSON:
76
+ print_json(jc)
77
+ else:
78
+ name = jc.get("name", "")
79
+ hostname = jc.get("hostname", "")
80
+ fqdn = jc.get("fqdn", "")
81
+ os_info = jc.get("operating_system", "")
82
+ private_ip = jc.get("private_ip", "")
83
+ public_ip = jc.get("public_ip", "")
84
+ connection = jc.get("connection_type", "")
85
+ console_user = jc.get("console_user", "") or "-"
86
+ last_connect = jc.get("last_connect_timestamp", "") or "-"
87
+ jump_group = jc.get("jump_group_id", "")
88
+ tag = jc.get("tag", "") or "-"
89
+
90
+ console.print(Panel(
91
+ f"[bold]{name}[/bold]\n\n"
92
+ f"[dim]Hostname:[/dim] {hostname}\n"
93
+ f"[dim]FQDN:[/dim] {fqdn}\n"
94
+ f"[dim]OS:[/dim] {os_info}\n"
95
+ f"[dim]Private IP:[/dim] {private_ip}\n"
96
+ f"[dim]Public IP:[/dim] {public_ip}\n"
97
+ f"[dim]Connection:[/dim] {connection}\n"
98
+ f"[dim]Console User:[/dim] {console_user}\n"
99
+ f"[dim]Last Connect:[/dim] {last_connect}\n"
100
+ f"[dim]Jump Group ID:[/dim] {jump_group}\n"
101
+ f"[dim]Tag:[/dim] {tag}",
102
+ title="Jump Client Details",
103
+ subtitle=f"ID: {jc.get('id', '')}",
104
+ ))
105
+ except httpx.HTTPStatusError as e:
106
+ print_api_error(e, "get jump client")
107
+ raise typer.Exit(1)
108
+ except httpx.RequestError as e:
109
+ print_api_error(e, "get jump client")
110
+ raise typer.Exit(1)
111
+ except Exception as e:
112
+ print_api_error(e, "get jump client")
113
+ raise typer.Exit(1)
114
+
115
+
116
+ @app.command("delete")
117
+ def delete_jump_client(
118
+ client_id: int = typer.Argument(..., help="Jump Client ID"),
119
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
120
+ ):
121
+ """Delete a Jump Client."""
122
+ from bt_cli.pra.client import get_client
123
+
124
+ if not force:
125
+ typer.confirm(f"Delete jump client {client_id}?", abort=True)
126
+
127
+ try:
128
+ client = get_client()
129
+ client.delete_jump_client(client_id)
130
+ print_success(f"Deleted jump client {client_id}")
131
+ except httpx.HTTPStatusError as e:
132
+ print_api_error(e, "delete jump client")
133
+ raise typer.Exit(1)
134
+ except httpx.RequestError as e:
135
+ print_api_error(e, "delete jump client")
136
+ raise typer.Exit(1)
137
+ except Exception as e:
138
+ print_api_error(e, "delete jump client")
139
+ raise typer.Exit(1)