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.
- bt_cli/__init__.py +3 -0
- bt_cli/cli.py +830 -0
- bt_cli/commands/__init__.py +1 -0
- bt_cli/commands/configure.py +415 -0
- bt_cli/commands/learn.py +229 -0
- bt_cli/commands/quick.py +784 -0
- bt_cli/core/__init__.py +1 -0
- bt_cli/core/auth.py +213 -0
- bt_cli/core/client.py +313 -0
- bt_cli/core/config.py +393 -0
- bt_cli/core/config_file.py +420 -0
- bt_cli/core/csv_utils.py +91 -0
- bt_cli/core/errors.py +247 -0
- bt_cli/core/output.py +205 -0
- bt_cli/core/prompts.py +87 -0
- bt_cli/core/rest_debug.py +221 -0
- bt_cli/data/CLAUDE.md +94 -0
- bt_cli/data/__init__.py +0 -0
- bt_cli/data/skills/bt/SKILL.md +108 -0
- bt_cli/data/skills/entitle/SKILL.md +170 -0
- bt_cli/data/skills/epmw/SKILL.md +144 -0
- bt_cli/data/skills/pra/SKILL.md +150 -0
- bt_cli/data/skills/pws/SKILL.md +198 -0
- bt_cli/entitle/__init__.py +1 -0
- bt_cli/entitle/client/__init__.py +5 -0
- bt_cli/entitle/client/base.py +443 -0
- bt_cli/entitle/commands/__init__.py +24 -0
- bt_cli/entitle/commands/accounts.py +53 -0
- bt_cli/entitle/commands/applications.py +39 -0
- bt_cli/entitle/commands/auth.py +68 -0
- bt_cli/entitle/commands/bundles.py +218 -0
- bt_cli/entitle/commands/integrations.py +60 -0
- bt_cli/entitle/commands/permissions.py +70 -0
- bt_cli/entitle/commands/policies.py +97 -0
- bt_cli/entitle/commands/resources.py +131 -0
- bt_cli/entitle/commands/roles.py +74 -0
- bt_cli/entitle/commands/users.py +123 -0
- bt_cli/entitle/commands/workflows.py +187 -0
- bt_cli/entitle/models/__init__.py +31 -0
- bt_cli/entitle/models/bundle.py +28 -0
- bt_cli/entitle/models/common.py +37 -0
- bt_cli/entitle/models/integration.py +30 -0
- bt_cli/entitle/models/permission.py +27 -0
- bt_cli/entitle/models/policy.py +25 -0
- bt_cli/entitle/models/resource.py +29 -0
- bt_cli/entitle/models/role.py +28 -0
- bt_cli/entitle/models/user.py +24 -0
- bt_cli/entitle/models/workflow.py +55 -0
- bt_cli/epmw/__init__.py +1 -0
- bt_cli/epmw/client/__init__.py +5 -0
- bt_cli/epmw/client/base.py +848 -0
- bt_cli/epmw/commands/__init__.py +33 -0
- bt_cli/epmw/commands/audits.py +250 -0
- bt_cli/epmw/commands/auth.py +55 -0
- bt_cli/epmw/commands/computers.py +140 -0
- bt_cli/epmw/commands/events.py +233 -0
- bt_cli/epmw/commands/groups.py +215 -0
- bt_cli/epmw/commands/policies.py +673 -0
- bt_cli/epmw/commands/quick.py +348 -0
- bt_cli/epmw/commands/requests.py +224 -0
- bt_cli/epmw/commands/roles.py +78 -0
- bt_cli/epmw/commands/tasks.py +38 -0
- bt_cli/epmw/commands/users.py +219 -0
- bt_cli/epmw/models/__init__.py +1 -0
- bt_cli/pra/__init__.py +1 -0
- bt_cli/pra/client/__init__.py +5 -0
- bt_cli/pra/client/base.py +618 -0
- bt_cli/pra/commands/__init__.py +30 -0
- bt_cli/pra/commands/auth.py +55 -0
- bt_cli/pra/commands/import_export.py +442 -0
- bt_cli/pra/commands/jump_clients.py +139 -0
- bt_cli/pra/commands/jump_groups.py +146 -0
- bt_cli/pra/commands/jump_items.py +638 -0
- bt_cli/pra/commands/jumpoints.py +95 -0
- bt_cli/pra/commands/policies.py +197 -0
- bt_cli/pra/commands/quick.py +470 -0
- bt_cli/pra/commands/teams.py +81 -0
- bt_cli/pra/commands/users.py +87 -0
- bt_cli/pra/commands/vault.py +564 -0
- bt_cli/pra/models/__init__.py +27 -0
- bt_cli/pra/models/common.py +12 -0
- bt_cli/pra/models/jump_client.py +25 -0
- bt_cli/pra/models/jump_group.py +15 -0
- bt_cli/pra/models/jump_item.py +72 -0
- bt_cli/pra/models/jumpoint.py +19 -0
- bt_cli/pra/models/team.py +14 -0
- bt_cli/pra/models/user.py +17 -0
- bt_cli/pra/models/vault.py +45 -0
- bt_cli/pws/__init__.py +1 -0
- bt_cli/pws/client/__init__.py +5 -0
- bt_cli/pws/client/base.py +356 -0
- bt_cli/pws/client/beyondinsight.py +869 -0
- bt_cli/pws/client/passwordsafe.py +1786 -0
- bt_cli/pws/commands/__init__.py +33 -0
- bt_cli/pws/commands/accounts.py +372 -0
- bt_cli/pws/commands/assets.py +311 -0
- bt_cli/pws/commands/auth.py +166 -0
- bt_cli/pws/commands/clouds.py +221 -0
- bt_cli/pws/commands/config.py +344 -0
- bt_cli/pws/commands/credentials.py +347 -0
- bt_cli/pws/commands/databases.py +306 -0
- bt_cli/pws/commands/directories.py +199 -0
- bt_cli/pws/commands/functional.py +298 -0
- bt_cli/pws/commands/import_export.py +452 -0
- bt_cli/pws/commands/platforms.py +118 -0
- bt_cli/pws/commands/quick.py +1646 -0
- bt_cli/pws/commands/search.py +256 -0
- bt_cli/pws/commands/secrets.py +1343 -0
- bt_cli/pws/commands/systems.py +389 -0
- bt_cli/pws/commands/users.py +415 -0
- bt_cli/pws/commands/workgroups.py +166 -0
- bt_cli/pws/config.py +18 -0
- bt_cli/pws/models/__init__.py +19 -0
- bt_cli/pws/models/account.py +186 -0
- bt_cli/pws/models/asset.py +102 -0
- bt_cli/pws/models/common.py +132 -0
- bt_cli/pws/models/system.py +121 -0
- bt_cli-0.4.13.dist-info/METADATA +417 -0
- bt_cli-0.4.13.dist-info/RECORD +121 -0
- bt_cli-0.4.13.dist-info/WHEEL +4 -0
- bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""CLI commands for credential checkout and management."""
|
|
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
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from ...core.output import print_error, print_api_error
|
|
13
|
+
from ..client.base import get_client
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(no_args_is_help=True, help="Checkout and manage credentials in Password Safe")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def print_requests_table(requests: list[dict], title: str = "Credential Requests") -> None:
|
|
20
|
+
"""Print requests in a formatted table."""
|
|
21
|
+
table = Table(title=title)
|
|
22
|
+
table.add_column("Request ID", style="cyan", no_wrap=True)
|
|
23
|
+
table.add_column("Account", style="green")
|
|
24
|
+
table.add_column("System", style="yellow")
|
|
25
|
+
table.add_column("Status", style="magenta")
|
|
26
|
+
table.add_column("Duration", style="blue")
|
|
27
|
+
table.add_column("Expires", style="dim")
|
|
28
|
+
|
|
29
|
+
for req in requests:
|
|
30
|
+
table.add_row(
|
|
31
|
+
str(req.get("RequestID", "")),
|
|
32
|
+
req.get("AccountName", str(req.get("AccountId", ""))),
|
|
33
|
+
req.get("SystemName", "-"),
|
|
34
|
+
req.get("Status", "-"),
|
|
35
|
+
f"{req.get('DurationMinutes', '-')} min",
|
|
36
|
+
str(req.get("ExpiresDate", "-")),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
console.print(table)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("checkout")
|
|
43
|
+
def checkout_credential(
|
|
44
|
+
system: str = typer.Option(..., "--system", "-s", help="Managed system name"),
|
|
45
|
+
account: str = typer.Option(..., "--account", "-a", help="Account name"),
|
|
46
|
+
duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
|
|
47
|
+
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for checkout"),
|
|
48
|
+
access_type: str = typer.Option("View", "--access-type", "-t", help="Access type (View, RDP, SSH)"),
|
|
49
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Checkout a credential (create a release request).
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
pws credentials checkout --system "WindowsServer" --account "Administrator"
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
with get_client() as client:
|
|
58
|
+
client.authenticate()
|
|
59
|
+
|
|
60
|
+
# Use the convenience method that handles system/account lookup
|
|
61
|
+
request = client.checkout_credential(
|
|
62
|
+
system=system,
|
|
63
|
+
account=account,
|
|
64
|
+
duration=duration,
|
|
65
|
+
reason=reason,
|
|
66
|
+
access_type=access_type,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
request_id = request.get("RequestID")
|
|
70
|
+
|
|
71
|
+
if output == "json":
|
|
72
|
+
console.print_json(json.dumps(request, default=str))
|
|
73
|
+
else:
|
|
74
|
+
console.print(Panel(
|
|
75
|
+
f"[green]Credential checkout request created![/green]\n\n"
|
|
76
|
+
f"Request ID: [bold cyan]{request_id}[/bold cyan]\n"
|
|
77
|
+
f"System: {system}\n"
|
|
78
|
+
f"Account: {account}\n"
|
|
79
|
+
f"Duration: {duration} minutes\n\n"
|
|
80
|
+
f"[dim]Use 'pws credentials show {request_id}' to get the password[/dim]",
|
|
81
|
+
title="Checkout Request",
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
print_error(f"Invalid input: {e}")
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
except httpx.HTTPStatusError as e:
|
|
88
|
+
print_api_error(e, "manage credentials")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
except httpx.RequestError as e:
|
|
91
|
+
print_api_error(e, "manage credentials")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print_api_error(e, "manage credentials")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command("show")
|
|
99
|
+
def show_credential(
|
|
100
|
+
request_id: int = typer.Argument(..., help="Request ID"),
|
|
101
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
102
|
+
raw: bool = typer.Option(False, "--raw", "-r", help="Output only the password (for scripts)"),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Get the password for an approved credential request.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
pws credentials show 12345
|
|
108
|
+
pws credentials show 12345 --raw # Just the password for scripts
|
|
109
|
+
PASSWORD=$(bt pws credentials show 12345 --raw)
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
with get_client() as client:
|
|
113
|
+
client.authenticate()
|
|
114
|
+
credential = client.get_credential(request_id)
|
|
115
|
+
|
|
116
|
+
if raw:
|
|
117
|
+
# Output just the password with no formatting (for scripts)
|
|
118
|
+
print(credential.get("Password", ""), end="")
|
|
119
|
+
elif output == "json":
|
|
120
|
+
console.print_json(json.dumps(credential, default=str))
|
|
121
|
+
else:
|
|
122
|
+
password = credential.get("Password", "N/A")
|
|
123
|
+
console.print(Panel(
|
|
124
|
+
f"Request ID: [bold cyan]{request_id}[/bold cyan]\n"
|
|
125
|
+
f"Password: [bold green]{password}[/bold green]",
|
|
126
|
+
title="Credential",
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
except httpx.HTTPStatusError as e:
|
|
130
|
+
print_api_error(e, "manage credentials")
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
except httpx.RequestError as e:
|
|
133
|
+
print_api_error(e, "manage credentials")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print_api_error(e, "manage credentials")
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("checkin")
|
|
141
|
+
def checkin_credential(
|
|
142
|
+
request_id: int = typer.Argument(..., help="Request ID to check in"),
|
|
143
|
+
rotate: bool = typer.Option(False, "--rotate", "-r", help="Rotate password on checkin"),
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Check in a credential (release the checkout).
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
pws credentials checkin 12345
|
|
149
|
+
pws credentials checkin 12345 --rotate
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
with get_client() as client:
|
|
153
|
+
client.authenticate()
|
|
154
|
+
|
|
155
|
+
if rotate:
|
|
156
|
+
client.rotate_on_checkin(request_id)
|
|
157
|
+
|
|
158
|
+
client.checkin_request(request_id)
|
|
159
|
+
console.print(f"[green]Credential checked in successfully.[/green]")
|
|
160
|
+
if rotate:
|
|
161
|
+
console.print(f"[yellow]Password will be rotated.[/yellow]")
|
|
162
|
+
|
|
163
|
+
except httpx.HTTPStatusError as e:
|
|
164
|
+
print_api_error(e, "manage credentials")
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
except httpx.RequestError as e:
|
|
167
|
+
print_api_error(e, "manage credentials")
|
|
168
|
+
raise typer.Exit(1)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print_api_error(e, "manage credentials")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command("rotate")
|
|
175
|
+
def rotate_credential(
|
|
176
|
+
request_id: int = typer.Argument(..., help="Request ID"),
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Mark a credential to rotate password on checkin.
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
pws credentials rotate 12345
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
with get_client() as client:
|
|
185
|
+
client.authenticate()
|
|
186
|
+
client.rotate_on_checkin(request_id)
|
|
187
|
+
console.print(f"[green]Password rotation scheduled for request {request_id}.[/green]")
|
|
188
|
+
|
|
189
|
+
except httpx.HTTPStatusError as e:
|
|
190
|
+
print_api_error(e, "manage credentials")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
except httpx.RequestError as e:
|
|
193
|
+
print_api_error(e, "manage credentials")
|
|
194
|
+
raise typer.Exit(1)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print_api_error(e, "manage credentials")
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.command("list")
|
|
201
|
+
def list_requests(
|
|
202
|
+
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status"),
|
|
203
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
|
|
204
|
+
fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
|
|
205
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
206
|
+
) -> None:
|
|
207
|
+
"""List credential release requests.
|
|
208
|
+
|
|
209
|
+
Examples:
|
|
210
|
+
bt pws credentials list # First 50 requests
|
|
211
|
+
bt pws credentials list --all # All requests
|
|
212
|
+
bt pws credentials list --status approved
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
with get_client() as client:
|
|
216
|
+
client.authenticate()
|
|
217
|
+
requests = client.list_requests(status=status, limit=None if fetch_all else limit)
|
|
218
|
+
|
|
219
|
+
if output == "json":
|
|
220
|
+
console.print_json(json.dumps(requests, default=str))
|
|
221
|
+
else:
|
|
222
|
+
if requests:
|
|
223
|
+
print_requests_table(requests)
|
|
224
|
+
if not fetch_all and len(requests) == limit:
|
|
225
|
+
console.print(f"[dim]Showing {len(requests)} results. Use --all to fetch all results.[/dim]")
|
|
226
|
+
else:
|
|
227
|
+
console.print("[yellow]No requests found.[/yellow]")
|
|
228
|
+
|
|
229
|
+
except httpx.HTTPStatusError as e:
|
|
230
|
+
print_api_error(e, "manage credentials")
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
except httpx.RequestError as e:
|
|
233
|
+
print_api_error(e, "manage credentials")
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print_api_error(e, "manage credentials")
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command("get")
|
|
241
|
+
def get_request(
|
|
242
|
+
request_id: int = typer.Argument(..., help="Request ID"),
|
|
243
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Get details of a credential request.
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
pws credentials get 12345
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
with get_client() as client:
|
|
252
|
+
client.authenticate()
|
|
253
|
+
request = client.get_request(request_id)
|
|
254
|
+
|
|
255
|
+
if output == "json":
|
|
256
|
+
console.print_json(json.dumps(request, default=str))
|
|
257
|
+
else:
|
|
258
|
+
console.print(f"\n[bold cyan]Request: {request_id}[/bold cyan]\n")
|
|
259
|
+
|
|
260
|
+
info_table = Table(show_header=False, box=None)
|
|
261
|
+
info_table.add_column("Field", style="dim")
|
|
262
|
+
info_table.add_column("Value")
|
|
263
|
+
|
|
264
|
+
fields = [
|
|
265
|
+
("Request ID", "RequestID"),
|
|
266
|
+
("Account ID", "AccountId"),
|
|
267
|
+
("Account Name", "AccountName"),
|
|
268
|
+
("System Name", "SystemName"),
|
|
269
|
+
("Status", "Status"),
|
|
270
|
+
("Access Type", "AccessType"),
|
|
271
|
+
("Duration (min)", "DurationMinutes"),
|
|
272
|
+
("Reason", "Reason"),
|
|
273
|
+
("Requested Date", "RequestedDate"),
|
|
274
|
+
("Approved Date", "ApprovedDate"),
|
|
275
|
+
("Expires Date", "ExpiresDate"),
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
for label, key in fields:
|
|
279
|
+
value = request.get(key)
|
|
280
|
+
if value is not None:
|
|
281
|
+
info_table.add_row(label, str(value))
|
|
282
|
+
|
|
283
|
+
console.print(info_table)
|
|
284
|
+
|
|
285
|
+
except httpx.HTTPStatusError as e:
|
|
286
|
+
print_api_error(e, "manage credentials")
|
|
287
|
+
raise typer.Exit(1)
|
|
288
|
+
except httpx.RequestError as e:
|
|
289
|
+
print_api_error(e, "manage credentials")
|
|
290
|
+
raise typer.Exit(1)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print_api_error(e, "manage credentials")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@app.command("approve")
|
|
297
|
+
def approve_request(
|
|
298
|
+
request_id: int = typer.Argument(..., help="Request ID to approve"),
|
|
299
|
+
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Approval reason"),
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Approve a pending credential request.
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
pws credentials approve 12345
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
with get_client() as client:
|
|
308
|
+
client.authenticate()
|
|
309
|
+
client.approve_request(request_id, reason=reason)
|
|
310
|
+
console.print(f"[green]Request {request_id} approved.[/green]")
|
|
311
|
+
|
|
312
|
+
except httpx.HTTPStatusError as e:
|
|
313
|
+
print_api_error(e, "manage credentials")
|
|
314
|
+
raise typer.Exit(1)
|
|
315
|
+
except httpx.RequestError as e:
|
|
316
|
+
print_api_error(e, "manage credentials")
|
|
317
|
+
raise typer.Exit(1)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
print_api_error(e, "manage credentials")
|
|
320
|
+
raise typer.Exit(1)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@app.command("deny")
|
|
324
|
+
def deny_request(
|
|
325
|
+
request_id: int = typer.Argument(..., help="Request ID to deny"),
|
|
326
|
+
reason: str = typer.Option(..., "--reason", "-r", help="Denial reason (required)"),
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Deny a pending credential request.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
pws credentials deny 12345 --reason "Not authorized"
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
with get_client() as client:
|
|
335
|
+
client.authenticate()
|
|
336
|
+
client.deny_request(request_id, reason=reason)
|
|
337
|
+
console.print(f"[yellow]Request {request_id} denied.[/yellow]")
|
|
338
|
+
|
|
339
|
+
except httpx.HTTPStatusError as e:
|
|
340
|
+
print_api_error(e, "manage credentials")
|
|
341
|
+
raise typer.Exit(1)
|
|
342
|
+
except httpx.RequestError as e:
|
|
343
|
+
print_api_error(e, "manage credentials")
|
|
344
|
+
raise typer.Exit(1)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print_api_error(e, "manage credentials")
|
|
347
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""CLI commands for managing databases."""
|
|
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
|
|
12
|
+
from ..client.base import get_client
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, help="Manage database instances")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
# Common database platform IDs
|
|
18
|
+
DATABASE_PLATFORMS = {
|
|
19
|
+
"postgresql": 79,
|
|
20
|
+
"postgres": 79,
|
|
21
|
+
"mysql": 10,
|
|
22
|
+
"mssql": 11,
|
|
23
|
+
"sqlserver": 11,
|
|
24
|
+
"oracle": 8,
|
|
25
|
+
"mongodb": 74,
|
|
26
|
+
"sybase": 9,
|
|
27
|
+
"saphana": 85,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def print_databases_table(databases: list[dict], title: str = "Databases") -> None:
|
|
32
|
+
"""Print databases in a formatted table."""
|
|
33
|
+
table = Table(title=title)
|
|
34
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
35
|
+
table.add_column("Instance", style="green")
|
|
36
|
+
table.add_column("Platform", style="yellow")
|
|
37
|
+
table.add_column("Port", style="magenta")
|
|
38
|
+
table.add_column("Asset ID", style="blue")
|
|
39
|
+
table.add_column("Default", style="dim")
|
|
40
|
+
|
|
41
|
+
for db in databases:
|
|
42
|
+
table.add_row(
|
|
43
|
+
str(db.get("DatabaseID", "")),
|
|
44
|
+
db.get("InstanceName", "-"),
|
|
45
|
+
str(db.get("PlatformID", "-")),
|
|
46
|
+
str(db.get("Port", "-")),
|
|
47
|
+
str(db.get("AssetID", "-")),
|
|
48
|
+
"Yes" if db.get("IsDefaultInstance") else "No",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
console.print(table)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def print_database_detail(db: dict) -> None:
|
|
55
|
+
"""Print detailed database information."""
|
|
56
|
+
console.print(f"\n[bold cyan]Database: {db.get('InstanceName', 'Unknown')}[/bold cyan]\n")
|
|
57
|
+
|
|
58
|
+
info_table = Table(show_header=False, box=None)
|
|
59
|
+
info_table.add_column("Field", style="dim")
|
|
60
|
+
info_table.add_column("Value")
|
|
61
|
+
|
|
62
|
+
fields = [
|
|
63
|
+
("ID", "DatabaseID"),
|
|
64
|
+
("Instance Name", "InstanceName"),
|
|
65
|
+
("Platform ID", "PlatformID"),
|
|
66
|
+
("Asset ID", "AssetID"),
|
|
67
|
+
("Port", "Port"),
|
|
68
|
+
("Default Instance", "IsDefaultInstance"),
|
|
69
|
+
("Version", "Version"),
|
|
70
|
+
("Template", "Template"),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for label, key in fields:
|
|
74
|
+
value = db.get(key)
|
|
75
|
+
if value is not None:
|
|
76
|
+
if key == "IsDefaultInstance":
|
|
77
|
+
value = "Yes" if value else "No"
|
|
78
|
+
info_table.add_row(label, str(value))
|
|
79
|
+
|
|
80
|
+
console.print(info_table)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command("list")
|
|
84
|
+
def list_databases(
|
|
85
|
+
asset: Optional[int] = typer.Option(None, "--asset", "-a", help="Filter by asset ID"),
|
|
86
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
|
|
87
|
+
fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
|
|
88
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""List databases (all or by asset).
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
bt pws databases list # First 50 databases
|
|
94
|
+
bt pws databases list --all # All databases
|
|
95
|
+
bt pws databases list -a 123 # Databases for asset 123
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
with get_client() as client:
|
|
99
|
+
client.authenticate()
|
|
100
|
+
databases = client.list_databases(asset_id=asset, limit=None if fetch_all else limit)
|
|
101
|
+
|
|
102
|
+
if output == "json":
|
|
103
|
+
console.print_json(json.dumps(databases, default=str))
|
|
104
|
+
else:
|
|
105
|
+
if databases:
|
|
106
|
+
print_databases_table(databases)
|
|
107
|
+
if not fetch_all and len(databases) == limit:
|
|
108
|
+
console.print(f"[dim]Showing {len(databases)} results. Use --all to fetch all results.[/dim]")
|
|
109
|
+
else:
|
|
110
|
+
console.print("[yellow]No databases found.[/yellow]")
|
|
111
|
+
|
|
112
|
+
except httpx.HTTPStatusError as e:
|
|
113
|
+
print_api_error(e, "manage databases")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
except httpx.RequestError as e:
|
|
116
|
+
print_api_error(e, "manage databases")
|
|
117
|
+
raise typer.Exit(1)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print_api_error(e, "manage databases")
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command("get")
|
|
124
|
+
def get_database(
|
|
125
|
+
database_id: int = typer.Argument(..., help="Database ID"),
|
|
126
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Get a database by ID."""
|
|
129
|
+
try:
|
|
130
|
+
with get_client() as client:
|
|
131
|
+
client.authenticate()
|
|
132
|
+
db = client.get_database(database_id)
|
|
133
|
+
|
|
134
|
+
if output == "json":
|
|
135
|
+
console.print_json(json.dumps(db, default=str))
|
|
136
|
+
else:
|
|
137
|
+
print_database_detail(db)
|
|
138
|
+
|
|
139
|
+
except httpx.HTTPStatusError as e:
|
|
140
|
+
print_api_error(e, "manage databases")
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
except httpx.RequestError as e:
|
|
143
|
+
print_api_error(e, "manage databases")
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print_api_error(e, "manage databases")
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@app.command("create")
|
|
151
|
+
def create_database(
|
|
152
|
+
asset_id: int = typer.Option(..., "--asset", "-a", help="Asset ID to create database on"),
|
|
153
|
+
platform: str = typer.Option(..., "--platform", "-p", help="Platform: postgresql, mysql, mssql, oracle, mongodb, or numeric ID"),
|
|
154
|
+
instance: str = typer.Option(..., "--instance", "-i", help="Database instance name"),
|
|
155
|
+
port: Optional[int] = typer.Option(None, "--port", help="Connection port (uses platform default if not specified)"),
|
|
156
|
+
default: bool = typer.Option(False, "--default", "-d", help="Mark as default instance"),
|
|
157
|
+
version: Optional[str] = typer.Option(None, "--version", "-v", help="Database version"),
|
|
158
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Create a database on an asset.
|
|
161
|
+
|
|
162
|
+
Database platforms: postgresql (79), mysql (10), mssql (11), oracle (8), mongodb (74)
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
# Resolve platform ID
|
|
166
|
+
if platform.isdigit():
|
|
167
|
+
platform_id = int(platform)
|
|
168
|
+
else:
|
|
169
|
+
platform_lower = platform.lower()
|
|
170
|
+
if platform_lower not in DATABASE_PLATFORMS:
|
|
171
|
+
console.print(f"[red]Unknown platform:[/red] {platform}")
|
|
172
|
+
console.print("Valid platforms: postgresql, mysql, mssql, oracle, mongodb, or numeric ID")
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
platform_id = DATABASE_PLATFORMS[platform_lower]
|
|
175
|
+
|
|
176
|
+
with get_client() as client:
|
|
177
|
+
client.authenticate()
|
|
178
|
+
db = client.create_database(
|
|
179
|
+
asset_id=asset_id,
|
|
180
|
+
platform_id=platform_id,
|
|
181
|
+
instance_name=instance,
|
|
182
|
+
port=port,
|
|
183
|
+
is_default_instance=default,
|
|
184
|
+
version=version,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if output == "json":
|
|
188
|
+
console.print_json(json.dumps(db, default=str))
|
|
189
|
+
else:
|
|
190
|
+
console.print(f"[green]Created database:[/green] {db.get('InstanceName', 'Unknown')}")
|
|
191
|
+
console.print(f" ID: {db.get('DatabaseID', 'N/A')}")
|
|
192
|
+
console.print(f" Platform: {db.get('PlatformID', 'N/A')}")
|
|
193
|
+
console.print(f" Port: {db.get('Port', 'N/A')}")
|
|
194
|
+
|
|
195
|
+
except httpx.HTTPStatusError as e:
|
|
196
|
+
print_api_error(e, "manage databases")
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
except httpx.RequestError as e:
|
|
199
|
+
print_api_error(e, "manage databases")
|
|
200
|
+
raise typer.Exit(1)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
print_api_error(e, "manage databases")
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command("delete")
|
|
207
|
+
def delete_database(
|
|
208
|
+
database_id: int = typer.Argument(..., help="Database ID to delete"),
|
|
209
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Delete a database."""
|
|
212
|
+
try:
|
|
213
|
+
with get_client() as client:
|
|
214
|
+
client.authenticate()
|
|
215
|
+
|
|
216
|
+
if not force:
|
|
217
|
+
db = client.get_database(database_id)
|
|
218
|
+
name = db.get("InstanceName", "Unknown")
|
|
219
|
+
confirm = typer.confirm(
|
|
220
|
+
f"Are you sure you want to delete database '{name}' (ID: {database_id})?"
|
|
221
|
+
)
|
|
222
|
+
if not confirm:
|
|
223
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
224
|
+
raise typer.Exit(0)
|
|
225
|
+
|
|
226
|
+
client.delete_database(database_id)
|
|
227
|
+
console.print(f"[green]Deleted database ID: {database_id}[/green]")
|
|
228
|
+
|
|
229
|
+
except httpx.HTTPStatusError as e:
|
|
230
|
+
print_api_error(e, "manage databases")
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
except httpx.RequestError as e:
|
|
233
|
+
print_api_error(e, "manage databases")
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print_api_error(e, "manage databases")
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command("add-system")
|
|
241
|
+
def add_managed_system(
|
|
242
|
+
database_id: int = typer.Option(..., "--database", "-d", help="Database ID"),
|
|
243
|
+
platform: str = typer.Option(..., "--platform", "-p", help="Platform: postgresql, mysql, mssql, oracle, mongodb, or numeric ID"),
|
|
244
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="System name"),
|
|
245
|
+
port: Optional[int] = typer.Option(None, "--port", help="Connection port"),
|
|
246
|
+
functional_account: Optional[int] = typer.Option(None, "--functional-account", "-f", help="Functional account ID for auto-management"),
|
|
247
|
+
description: Optional[str] = typer.Option(None, "--description", help="Description"),
|
|
248
|
+
auto_manage: bool = typer.Option(False, "--auto-manage", help="Enable automatic password management"),
|
|
249
|
+
change_frequency: Optional[str] = typer.Option(None, "--change-frequency", help="Change frequency: first, last, or xdays"),
|
|
250
|
+
change_days: Optional[int] = typer.Option(30, "--change-days", help="Days between changes (if xdays)"),
|
|
251
|
+
change_time: Optional[str] = typer.Option("23:30", "--change-time", help="Time for changes (HH:MM)"),
|
|
252
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Create a managed system for a database.
|
|
255
|
+
|
|
256
|
+
This is used for database platforms like PostgreSQL, MySQL, Oracle, etc.
|
|
257
|
+
The database must be created first using 'pws databases create'.
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
pws databases add-system -d 2 -p postgresql -f 8 --auto-manage --change-frequency xdays
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
# Resolve platform ID
|
|
264
|
+
if platform.isdigit():
|
|
265
|
+
platform_id = int(platform)
|
|
266
|
+
else:
|
|
267
|
+
platform_lower = platform.lower()
|
|
268
|
+
if platform_lower not in DATABASE_PLATFORMS:
|
|
269
|
+
console.print(f"[red]Unknown platform:[/red] {platform}")
|
|
270
|
+
console.print("Valid platforms: postgresql, mysql, mssql, oracle, mongodb, or numeric ID")
|
|
271
|
+
raise typer.Exit(1)
|
|
272
|
+
platform_id = DATABASE_PLATFORMS[platform_lower]
|
|
273
|
+
|
|
274
|
+
with get_client() as client:
|
|
275
|
+
client.authenticate()
|
|
276
|
+
system = client.create_database_managed_system(
|
|
277
|
+
database_id=database_id,
|
|
278
|
+
platform_id=platform_id,
|
|
279
|
+
system_name=name,
|
|
280
|
+
port=port,
|
|
281
|
+
functional_account_id=functional_account,
|
|
282
|
+
description=description,
|
|
283
|
+
auto_management_flag=auto_manage,
|
|
284
|
+
change_frequency_type=change_frequency,
|
|
285
|
+
change_frequency_days=change_days,
|
|
286
|
+
change_time=change_time,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if output == "json":
|
|
290
|
+
console.print_json(json.dumps(system, default=str))
|
|
291
|
+
else:
|
|
292
|
+
console.print(f"[green]Created managed system:[/green] {system.get('SystemName', 'Unknown')}")
|
|
293
|
+
console.print(f" ID: {system.get('ManagedSystemID', 'N/A')}")
|
|
294
|
+
console.print(f" Database ID: {system.get('DatabaseID', 'N/A')}")
|
|
295
|
+
console.print(f" Platform: {system.get('PlatformID', 'N/A')}")
|
|
296
|
+
console.print(f" Auto-Management: {'Yes' if system.get('AutoManagementFlag') else 'No'}")
|
|
297
|
+
|
|
298
|
+
except httpx.HTTPStatusError as e:
|
|
299
|
+
print_api_error(e, "manage databases")
|
|
300
|
+
raise typer.Exit(1)
|
|
301
|
+
except httpx.RequestError as e:
|
|
302
|
+
print_api_error(e, "manage databases")
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print_api_error(e, "manage databases")
|
|
306
|
+
raise typer.Exit(1)
|