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,348 @@
|
|
|
1
|
+
"""Quick commands for EPM Windows - combine multiple API calls into single operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ...core.output import print_api_error, print_error, print_warning, print_success
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("stale")
|
|
19
|
+
def stale_computers(
|
|
20
|
+
hours: int = typer.Option(24, "--hours", "-h", help="Hours since last checkin (default: 24)"),
|
|
21
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Filter by group name (partial match)"),
|
|
22
|
+
delete: bool = typer.Option(False, "--delete", help="Delete stale computers after listing"),
|
|
23
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation when deleting"),
|
|
24
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Find computers that haven't checked in recently.
|
|
27
|
+
|
|
28
|
+
Shows computers that haven't connected in the specified number of hours.
|
|
29
|
+
Optionally filter by group name. Use --delete to remove stale computers.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
bt epmw quick stale # Not checked in for 24+ hours
|
|
33
|
+
bt epmw quick stale --hours 48 # Not checked in for 48+ hours
|
|
34
|
+
bt epmw quick stale -h 12 -g "Workstations" # Workstations not seen for 12+ hours
|
|
35
|
+
bt epmw quick stale -o json # JSON output for scripting
|
|
36
|
+
bt epmw quick stale --hours 72 --delete # Delete computers not seen for 72+ hours
|
|
37
|
+
bt epmw quick stale --delete --force # Delete without confirmation
|
|
38
|
+
"""
|
|
39
|
+
from ..client import get_client
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
client = get_client()
|
|
43
|
+
|
|
44
|
+
console.print(f"[dim]Finding computers not seen in {hours}+ hours...[/dim]")
|
|
45
|
+
computers = client.list_computers()
|
|
46
|
+
|
|
47
|
+
now = datetime.now(timezone.utc)
|
|
48
|
+
stale_computers = []
|
|
49
|
+
|
|
50
|
+
for comp in computers:
|
|
51
|
+
# Parse last connected time
|
|
52
|
+
last_connected_str = comp.get("lastConnected")
|
|
53
|
+
if not last_connected_str:
|
|
54
|
+
# Never connected - consider stale
|
|
55
|
+
stale_computers.append(comp)
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Parse ISO format datetime
|
|
60
|
+
last_connected = datetime.fromisoformat(last_connected_str.replace("Z", "+00:00"))
|
|
61
|
+
hours_since = (now - last_connected).total_seconds() / 3600
|
|
62
|
+
|
|
63
|
+
if hours_since >= hours:
|
|
64
|
+
comp["_hours_since"] = round(hours_since, 1)
|
|
65
|
+
stale_computers.append(comp)
|
|
66
|
+
except (ValueError, TypeError):
|
|
67
|
+
# Can't parse date - include it
|
|
68
|
+
comp["_hours_since"] = None
|
|
69
|
+
stale_computers.append(comp)
|
|
70
|
+
|
|
71
|
+
# Filter by group if specified
|
|
72
|
+
if group:
|
|
73
|
+
group_lower = group.lower()
|
|
74
|
+
stale_computers = [
|
|
75
|
+
c for c in stale_computers
|
|
76
|
+
if group_lower in (c.get("groupName") or "").lower()
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
if output == "json":
|
|
80
|
+
# Clean up internal field for JSON output
|
|
81
|
+
for comp in stale_computers:
|
|
82
|
+
if "_hours_since" in comp:
|
|
83
|
+
comp["hoursSinceLastCheckin"] = comp.pop("_hours_since")
|
|
84
|
+
console.print_json(json.dumps(stale_computers, default=str))
|
|
85
|
+
else:
|
|
86
|
+
if stale_computers:
|
|
87
|
+
title = f"Computers not seen in {hours}+ hours"
|
|
88
|
+
if group:
|
|
89
|
+
title += f" (group: {group})"
|
|
90
|
+
|
|
91
|
+
table = Table(title=title)
|
|
92
|
+
table.add_column("Host", style="cyan")
|
|
93
|
+
table.add_column("Domain", style="yellow")
|
|
94
|
+
table.add_column("Group", style="green")
|
|
95
|
+
table.add_column("Hours Since", style="red", justify="right")
|
|
96
|
+
table.add_column("Last Connected", style="dim")
|
|
97
|
+
table.add_column("Status", style="magenta")
|
|
98
|
+
|
|
99
|
+
for comp in stale_computers:
|
|
100
|
+
hours_since = comp.get("_hours_since")
|
|
101
|
+
hours_display = f"{hours_since:.1f}" if hours_since is not None else "N/A"
|
|
102
|
+
|
|
103
|
+
last_connected = comp.get("lastConnected", "Never")
|
|
104
|
+
if last_connected and last_connected != "Never":
|
|
105
|
+
# Format to readable date
|
|
106
|
+
try:
|
|
107
|
+
dt = datetime.fromisoformat(last_connected.replace("Z", "+00:00"))
|
|
108
|
+
last_connected = dt.strftime("%Y-%m-%d %H:%M")
|
|
109
|
+
except (ValueError, TypeError):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
table.add_row(
|
|
113
|
+
comp.get("host", ""),
|
|
114
|
+
comp.get("domain", "") or "-",
|
|
115
|
+
comp.get("groupName", "") or "-",
|
|
116
|
+
hours_display,
|
|
117
|
+
last_connected,
|
|
118
|
+
comp.get("connectionStatus", ""),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
console.print(table)
|
|
122
|
+
console.print(f"\n[dim]Total: {len(stale_computers)} computer(s)[/dim]")
|
|
123
|
+
|
|
124
|
+
# Delete stale computers if requested
|
|
125
|
+
if delete and stale_computers:
|
|
126
|
+
console.print()
|
|
127
|
+
if not force:
|
|
128
|
+
confirm = typer.confirm(
|
|
129
|
+
f"Delete {len(stale_computers)} stale computer(s)?"
|
|
130
|
+
)
|
|
131
|
+
if not confirm:
|
|
132
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
133
|
+
raise typer.Exit(0)
|
|
134
|
+
|
|
135
|
+
console.print("[dim]Deleting stale computers...[/dim]")
|
|
136
|
+
deleted_count = 0
|
|
137
|
+
failed_count = 0
|
|
138
|
+
for comp in stale_computers:
|
|
139
|
+
comp_id = comp.get("id")
|
|
140
|
+
comp_host = comp.get("host", "Unknown")
|
|
141
|
+
try:
|
|
142
|
+
client.delete_computer(comp_id)
|
|
143
|
+
console.print(f" [green]Deleted:[/green] {comp_host}")
|
|
144
|
+
deleted_count += 1
|
|
145
|
+
except Exception as e:
|
|
146
|
+
console.print(f" [red]Failed:[/red] {comp_host} - {e}")
|
|
147
|
+
failed_count += 1
|
|
148
|
+
|
|
149
|
+
console.print()
|
|
150
|
+
print_success(f"Deleted {deleted_count} computer(s)")
|
|
151
|
+
if failed_count > 0:
|
|
152
|
+
print_warning(f"Failed to delete {failed_count} computer(s)")
|
|
153
|
+
else:
|
|
154
|
+
msg = f"All computers have checked in within the last {hours} hours"
|
|
155
|
+
if group:
|
|
156
|
+
msg += f" (in group matching '{group}')"
|
|
157
|
+
console.print(f"[green]{msg}[/green]")
|
|
158
|
+
|
|
159
|
+
except httpx.HTTPStatusError as e:
|
|
160
|
+
print_api_error(e, "quick stale")
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
except httpx.RequestError as e:
|
|
163
|
+
print_api_error(e, "quick stale")
|
|
164
|
+
raise typer.Exit(1)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print_api_error(e, "quick stale")
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.command("disconnected")
|
|
171
|
+
def disconnected_computers(
|
|
172
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Filter by group name (partial match)"),
|
|
173
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Find computers with disconnected status.
|
|
176
|
+
|
|
177
|
+
Shows all computers that are currently marked as disconnected.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
bt epmw quick disconnected
|
|
181
|
+
bt epmw quick disconnected -g "Servers"
|
|
182
|
+
bt epmw quick disconnected -o json
|
|
183
|
+
"""
|
|
184
|
+
from ..client import get_client
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
client = get_client()
|
|
188
|
+
|
|
189
|
+
console.print("[dim]Finding disconnected computers...[/dim]")
|
|
190
|
+
computers = client.list_computers()
|
|
191
|
+
|
|
192
|
+
# Filter to disconnected status
|
|
193
|
+
disconnected = [
|
|
194
|
+
c for c in computers
|
|
195
|
+
if c.get("connectionStatus", "").lower() == "disconnected"
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
# Filter by group if specified
|
|
199
|
+
if group:
|
|
200
|
+
group_lower = group.lower()
|
|
201
|
+
disconnected = [
|
|
202
|
+
c for c in disconnected
|
|
203
|
+
if group_lower in (c.get("groupName") or "").lower()
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
if output == "json":
|
|
207
|
+
console.print_json(json.dumps(disconnected, default=str))
|
|
208
|
+
else:
|
|
209
|
+
if disconnected:
|
|
210
|
+
title = "Disconnected Computers"
|
|
211
|
+
if group:
|
|
212
|
+
title += f" (group: {group})"
|
|
213
|
+
|
|
214
|
+
table = Table(title=title)
|
|
215
|
+
table.add_column("Host", style="cyan")
|
|
216
|
+
table.add_column("Domain", style="yellow")
|
|
217
|
+
table.add_column("Group", style="green")
|
|
218
|
+
table.add_column("Days Disconnected", style="red", justify="right")
|
|
219
|
+
table.add_column("Last Connected", style="dim")
|
|
220
|
+
|
|
221
|
+
for comp in disconnected:
|
|
222
|
+
days_disc = comp.get("daysDisconnected", 0)
|
|
223
|
+
|
|
224
|
+
last_connected = comp.get("lastConnected", "Never")
|
|
225
|
+
if last_connected and last_connected != "Never":
|
|
226
|
+
try:
|
|
227
|
+
dt = datetime.fromisoformat(last_connected.replace("Z", "+00:00"))
|
|
228
|
+
last_connected = dt.strftime("%Y-%m-%d %H:%M")
|
|
229
|
+
except (ValueError, TypeError):
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
table.add_row(
|
|
233
|
+
comp.get("host", ""),
|
|
234
|
+
comp.get("domain", "") or "-",
|
|
235
|
+
comp.get("groupName", "") or "-",
|
|
236
|
+
str(days_disc),
|
|
237
|
+
last_connected,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
console.print(table)
|
|
241
|
+
console.print(f"\n[dim]Total: {len(disconnected)} computer(s)[/dim]")
|
|
242
|
+
else:
|
|
243
|
+
msg = "No disconnected computers found"
|
|
244
|
+
if group:
|
|
245
|
+
msg += f" in group matching '{group}'"
|
|
246
|
+
console.print(f"[green]{msg}[/green]")
|
|
247
|
+
|
|
248
|
+
except httpx.HTTPStatusError as e:
|
|
249
|
+
print_api_error(e, "quick disconnected")
|
|
250
|
+
raise typer.Exit(1)
|
|
251
|
+
except httpx.RequestError as e:
|
|
252
|
+
print_api_error(e, "quick disconnected")
|
|
253
|
+
raise typer.Exit(1)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print_api_error(e, "quick disconnected")
|
|
256
|
+
raise typer.Exit(1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@app.command("status")
|
|
260
|
+
def group_status(
|
|
261
|
+
group: Optional[str] = typer.Option(None, "--group", "-g", help="Filter by group name (partial match)"),
|
|
262
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Show computer status summary by group.
|
|
265
|
+
|
|
266
|
+
Displays counts of connected/disconnected computers per group.
|
|
267
|
+
|
|
268
|
+
Examples:
|
|
269
|
+
bt epmw quick status
|
|
270
|
+
bt epmw quick status -g "Datacenter"
|
|
271
|
+
bt epmw quick status -o json
|
|
272
|
+
"""
|
|
273
|
+
from ..client import get_client
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
client = get_client()
|
|
277
|
+
|
|
278
|
+
console.print("[dim]Getting computer status by group...[/dim]")
|
|
279
|
+
computers = client.list_computers()
|
|
280
|
+
|
|
281
|
+
# Filter by group if specified
|
|
282
|
+
if group:
|
|
283
|
+
group_lower = group.lower()
|
|
284
|
+
computers = [
|
|
285
|
+
c for c in computers
|
|
286
|
+
if group_lower in (c.get("groupName") or "").lower()
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
# Group by group name
|
|
290
|
+
groups: dict = {}
|
|
291
|
+
for comp in computers:
|
|
292
|
+
group_name = comp.get("groupName") or "(No Group)"
|
|
293
|
+
if group_name not in groups:
|
|
294
|
+
groups[group_name] = {"connected": 0, "disconnected": 0, "total": 0}
|
|
295
|
+
|
|
296
|
+
groups[group_name]["total"] += 1
|
|
297
|
+
status = comp.get("connectionStatus", "").lower()
|
|
298
|
+
if status == "connected":
|
|
299
|
+
groups[group_name]["connected"] += 1
|
|
300
|
+
else:
|
|
301
|
+
groups[group_name]["disconnected"] += 1
|
|
302
|
+
|
|
303
|
+
if output == "json":
|
|
304
|
+
console.print_json(json.dumps(groups, default=str))
|
|
305
|
+
else:
|
|
306
|
+
if groups:
|
|
307
|
+
table = Table(title="Computer Status by Group")
|
|
308
|
+
table.add_column("Group", style="cyan")
|
|
309
|
+
table.add_column("Connected", style="green", justify="right")
|
|
310
|
+
table.add_column("Disconnected", style="red", justify="right")
|
|
311
|
+
table.add_column("Total", style="bold", justify="right")
|
|
312
|
+
|
|
313
|
+
total_connected = 0
|
|
314
|
+
total_disconnected = 0
|
|
315
|
+
total_all = 0
|
|
316
|
+
|
|
317
|
+
for group_name, counts in sorted(groups.items()):
|
|
318
|
+
table.add_row(
|
|
319
|
+
group_name,
|
|
320
|
+
str(counts["connected"]),
|
|
321
|
+
str(counts["disconnected"]),
|
|
322
|
+
str(counts["total"]),
|
|
323
|
+
)
|
|
324
|
+
total_connected += counts["connected"]
|
|
325
|
+
total_disconnected += counts["disconnected"]
|
|
326
|
+
total_all += counts["total"]
|
|
327
|
+
|
|
328
|
+
# Add totals row
|
|
329
|
+
table.add_row(
|
|
330
|
+
"[bold]TOTAL[/bold]",
|
|
331
|
+
f"[bold green]{total_connected}[/bold green]",
|
|
332
|
+
f"[bold red]{total_disconnected}[/bold red]",
|
|
333
|
+
f"[bold]{total_all}[/bold]",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
console.print(table)
|
|
337
|
+
else:
|
|
338
|
+
console.print("[yellow]No computers found[/yellow]")
|
|
339
|
+
|
|
340
|
+
except httpx.HTTPStatusError as e:
|
|
341
|
+
print_api_error(e, "quick status")
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
except httpx.RequestError as e:
|
|
344
|
+
print_api_error(e, "quick status")
|
|
345
|
+
raise typer.Exit(1)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
print_api_error(e, "quick status")
|
|
348
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""EPMW admin access request commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from bt_cli.core.output import OutputFormat, print_api_error, print_json, print_table
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True, help="Admin access requests")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("list")
|
|
14
|
+
def list_requests(
|
|
15
|
+
output: OutputFormat = typer.Option(
|
|
16
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
17
|
+
),
|
|
18
|
+
):
|
|
19
|
+
"""List admin access requests."""
|
|
20
|
+
from bt_cli.epmw.client import get_client
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
client = get_client()
|
|
24
|
+
requests = client.list_admin_requests()
|
|
25
|
+
|
|
26
|
+
if output == OutputFormat.JSON:
|
|
27
|
+
print_json(requests)
|
|
28
|
+
else:
|
|
29
|
+
# Flatten the nested structure for table display
|
|
30
|
+
rows = []
|
|
31
|
+
for req in requests:
|
|
32
|
+
info = req.get("requestInfo", {})
|
|
33
|
+
decision = req.get("accessDecision", {})
|
|
34
|
+
rows.append({
|
|
35
|
+
"ticketId": info.get("ticketId"),
|
|
36
|
+
"userName": info.get("userName"),
|
|
37
|
+
"reason": info.get("reason", "")[:50],
|
|
38
|
+
"status": decision.get("status"),
|
|
39
|
+
"duration": decision.get("duration"),
|
|
40
|
+
"requestId": info.get("requestId"),
|
|
41
|
+
})
|
|
42
|
+
columns = [
|
|
43
|
+
("Ticket", "ticketId"),
|
|
44
|
+
("Request ID", "requestId"),
|
|
45
|
+
("User", "userName"),
|
|
46
|
+
("Reason", "reason"),
|
|
47
|
+
("Status", "status"),
|
|
48
|
+
("Duration", "duration"),
|
|
49
|
+
]
|
|
50
|
+
print_table(rows, columns, title="Admin Access Requests (use Request ID for approve/deny)")
|
|
51
|
+
except httpx.HTTPStatusError as e:
|
|
52
|
+
print_api_error(e, "list requests")
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
except httpx.RequestError as e:
|
|
55
|
+
print_api_error(e, "list requests")
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print_api_error(e, "list requests")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command("get")
|
|
63
|
+
def get_request(
|
|
64
|
+
request_id: str = typer.Argument(..., help="Request ID"),
|
|
65
|
+
output: OutputFormat = typer.Option(
|
|
66
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
67
|
+
),
|
|
68
|
+
):
|
|
69
|
+
"""Get admin access request details."""
|
|
70
|
+
from bt_cli.epmw.client import get_client
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
client = get_client()
|
|
74
|
+
request = client.get_admin_request(request_id)
|
|
75
|
+
|
|
76
|
+
if output == OutputFormat.JSON:
|
|
77
|
+
print_json(request)
|
|
78
|
+
else:
|
|
79
|
+
info = request.get("requestInfo", {})
|
|
80
|
+
decision = request.get("accessDecision", {})
|
|
81
|
+
typer.echo(f"Request ID: {info.get('requestId')}")
|
|
82
|
+
typer.echo(f"Ticket ID: {info.get('ticketId')}")
|
|
83
|
+
typer.echo(f"User: {info.get('userName')}")
|
|
84
|
+
typer.echo(f"Reason: {info.get('reason')}")
|
|
85
|
+
typer.echo(f"Duration Requested: {info.get('durationRequested')} seconds")
|
|
86
|
+
typer.echo(f"Status: {decision.get('status')}")
|
|
87
|
+
typer.echo(f"Duration: {decision.get('duration')}")
|
|
88
|
+
if decision.get('startTime'):
|
|
89
|
+
typer.echo(f"Start Time: {decision.get('startTime')}")
|
|
90
|
+
if decision.get('endTime'):
|
|
91
|
+
typer.echo(f"End Time: {decision.get('endTime')}")
|
|
92
|
+
except httpx.HTTPStatusError as e:
|
|
93
|
+
print_api_error(e, "get request")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
except httpx.RequestError as e:
|
|
96
|
+
print_api_error(e, "get request")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print_api_error(e, "get request")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command("create")
|
|
104
|
+
def create_request(
|
|
105
|
+
computer_id: str = typer.Option(..., "--computer", "-c", help="Computer ID"),
|
|
106
|
+
user_id: str = typer.Option(..., "--user-id", "-u", help="User SID (e.g., S-1-5-21-...)"),
|
|
107
|
+
user_name: str = typer.Option(..., "--user-name", "-n", help="User name (e.g., DOMAIN\\username)"),
|
|
108
|
+
reason: str = typer.Option(..., "--reason", "-r", help="Reason for request"),
|
|
109
|
+
duration: int = typer.Option(1800, "--duration", "-d", help="Duration in seconds (default: 1800 = 30 min)"),
|
|
110
|
+
output: OutputFormat = typer.Option(
|
|
111
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
112
|
+
),
|
|
113
|
+
):
|
|
114
|
+
"""Create an admin access request.
|
|
115
|
+
|
|
116
|
+
Note: This creates a request on behalf of a user. You need the user's
|
|
117
|
+
SID and domain\\username from the target computer.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
|
|
121
|
+
# 30 minute request (default)
|
|
122
|
+
bt epmw requests create \\
|
|
123
|
+
-c e4b4453a-302c-45c4-b64d-272e632f2beb \\
|
|
124
|
+
-u "S-1-5-21-1897974175-2172897935-264522243-4835" \\
|
|
125
|
+
-n "NEXUSDYN\\\\stan.power" \\
|
|
126
|
+
-r "Need to install software"
|
|
127
|
+
|
|
128
|
+
# 1 hour request
|
|
129
|
+
bt epmw requests create ... --duration 3600
|
|
130
|
+
"""
|
|
131
|
+
from bt_cli.epmw.client import get_client
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
client = get_client()
|
|
135
|
+
data = {
|
|
136
|
+
"computerId": computer_id,
|
|
137
|
+
"userId": user_id,
|
|
138
|
+
"userName": user_name,
|
|
139
|
+
"reason": reason,
|
|
140
|
+
"duration": str(duration), # API expects string
|
|
141
|
+
}
|
|
142
|
+
request = client.create_admin_request(data)
|
|
143
|
+
|
|
144
|
+
typer.echo(f"Created admin access request: {request.get('requestId', 'OK')}")
|
|
145
|
+
if output == OutputFormat.JSON:
|
|
146
|
+
print_json(request)
|
|
147
|
+
except httpx.HTTPStatusError as e:
|
|
148
|
+
print_api_error(e, "create request")
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
except httpx.RequestError as e:
|
|
151
|
+
print_api_error(e, "create request")
|
|
152
|
+
raise typer.Exit(1)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print_api_error(e, "create request")
|
|
155
|
+
raise typer.Exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command("approve")
|
|
159
|
+
def approve_request(
|
|
160
|
+
request_id: str = typer.Argument(..., help="Request ID (UUID from 'requests list')"),
|
|
161
|
+
message: str = typer.Option(..., "--message", "-m", help="Approval message (required)"),
|
|
162
|
+
duration: int = typer.Option(1800, "--duration", "-d", help="Approval duration in seconds (default: 1800 = 30 min)"),
|
|
163
|
+
performed_by: str = typer.Option("api-admin", "--performed-by", "-p", help="Username performing the decision"),
|
|
164
|
+
):
|
|
165
|
+
"""Approve an admin access request.
|
|
166
|
+
|
|
167
|
+
Use the Request ID (UUID) from 'bt epmw requests list', not the ticket ID.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
|
|
171
|
+
bt epmw requests approve 26c08294-9d58-4927-99a4-477186a125f4 -m "Approved for maintenance"
|
|
172
|
+
|
|
173
|
+
bt epmw requests approve <request-id> -m "Approved" -d 3600 # 1 hour
|
|
174
|
+
|
|
175
|
+
bt epmw requests approve <request-id> -m "Approved" -p "admin@example.com"
|
|
176
|
+
"""
|
|
177
|
+
from bt_cli.epmw.client import get_client
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
client = get_client()
|
|
181
|
+
result = client.approve_admin_request(request_id, message, duration, performed_by)
|
|
182
|
+
typer.echo(f"Approved request: {result.get('requestId', request_id)}")
|
|
183
|
+
except httpx.HTTPStatusError as e:
|
|
184
|
+
print_api_error(e, "approve request")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
except httpx.RequestError as e:
|
|
187
|
+
print_api_error(e, "approve request")
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print_api_error(e, "approve request")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.command("deny")
|
|
195
|
+
def deny_request(
|
|
196
|
+
request_id: str = typer.Argument(..., help="Request ID (UUID from 'requests list')"),
|
|
197
|
+
message: str = typer.Option(..., "--message", "-m", help="Denial message (required)"),
|
|
198
|
+
performed_by: str = typer.Option("api-admin", "--performed-by", "-p", help="Username performing the decision"),
|
|
199
|
+
):
|
|
200
|
+
"""Deny an admin access request.
|
|
201
|
+
|
|
202
|
+
Use the Request ID (UUID) from 'bt epmw requests list', not the ticket ID.
|
|
203
|
+
|
|
204
|
+
Examples:
|
|
205
|
+
|
|
206
|
+
bt epmw requests deny ab2b60d8-5375-459a-89a5-64565ef21691 -m "Not authorized"
|
|
207
|
+
|
|
208
|
+
bt epmw requests deny <request-id> -m "Denied" -p "admin@example.com"
|
|
209
|
+
"""
|
|
210
|
+
from bt_cli.epmw.client import get_client
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
client = get_client()
|
|
214
|
+
result = client.deny_admin_request(request_id, message, performed_by)
|
|
215
|
+
typer.echo(f"Denied request: {result.get('requestId', request_id)}")
|
|
216
|
+
except httpx.HTTPStatusError as e:
|
|
217
|
+
print_api_error(e, "deny request")
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
except httpx.RequestError as e:
|
|
220
|
+
print_api_error(e, "deny request")
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print_api_error(e, "deny request")
|
|
224
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""EPMW roles commands (read-only)."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from bt_cli.core.output import OutputFormat, print_api_error, print_json, print_table
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True, help="User roles and permissions")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("list")
|
|
12
|
+
def list_roles(
|
|
13
|
+
output: OutputFormat = typer.Option(
|
|
14
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
15
|
+
),
|
|
16
|
+
):
|
|
17
|
+
"""List all roles."""
|
|
18
|
+
from bt_cli.epmw.client import get_client
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
client = get_client()
|
|
22
|
+
roles = client.list_roles()
|
|
23
|
+
|
|
24
|
+
if output == OutputFormat.JSON:
|
|
25
|
+
print_json(roles)
|
|
26
|
+
else:
|
|
27
|
+
columns = [
|
|
28
|
+
("ID", "id"),
|
|
29
|
+
("Name", "name"),
|
|
30
|
+
]
|
|
31
|
+
print_table(roles, columns, title="Roles")
|
|
32
|
+
except httpx.HTTPStatusError as e:
|
|
33
|
+
print_api_error(e, "list roles")
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
except httpx.RequestError as e:
|
|
36
|
+
print_api_error(e, "list roles")
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print_api_error(e, "list roles")
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("get")
|
|
44
|
+
def get_role(
|
|
45
|
+
role_id: str = typer.Argument(..., help="Role ID"),
|
|
46
|
+
output: OutputFormat = typer.Option(
|
|
47
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
48
|
+
),
|
|
49
|
+
):
|
|
50
|
+
"""Get role details."""
|
|
51
|
+
from bt_cli.epmw.client import get_client
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
client = get_client()
|
|
55
|
+
role = client.get_role(role_id)
|
|
56
|
+
|
|
57
|
+
if output == OutputFormat.JSON:
|
|
58
|
+
print_json(role)
|
|
59
|
+
else:
|
|
60
|
+
typer.echo(f"ID: {role.get('id')}")
|
|
61
|
+
typer.echo(f"Name: {role.get('name')}")
|
|
62
|
+
if role.get('allowPermissions'):
|
|
63
|
+
typer.echo("Allow Permissions:")
|
|
64
|
+
for perm in role['allowPermissions']:
|
|
65
|
+
typer.echo(f" - {perm.get('resource')}: {perm.get('action')}")
|
|
66
|
+
if role.get('denyPermissions'):
|
|
67
|
+
typer.echo("Deny Permissions:")
|
|
68
|
+
for perm in role['denyPermissions']:
|
|
69
|
+
typer.echo(f" - {perm.get('resource')}: {perm.get('action')}")
|
|
70
|
+
except httpx.HTTPStatusError as e:
|
|
71
|
+
print_api_error(e, "get role")
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
except httpx.RequestError as e:
|
|
74
|
+
print_api_error(e, "get role")
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print_api_error(e, "get role")
|
|
78
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""EPMW task commands."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from bt_cli.core.output import OutputFormat, print_api_error, print_json
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True, help="Async task status")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("get")
|
|
12
|
+
def get_task(
|
|
13
|
+
task_id: str = typer.Argument(..., help="Task ID"),
|
|
14
|
+
output: OutputFormat = typer.Option(
|
|
15
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
16
|
+
),
|
|
17
|
+
):
|
|
18
|
+
"""Get task status."""
|
|
19
|
+
from bt_cli.epmw.client import get_client
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
client = get_client()
|
|
23
|
+
task = client.get_task(task_id)
|
|
24
|
+
|
|
25
|
+
if output == OutputFormat.JSON:
|
|
26
|
+
print_json(task)
|
|
27
|
+
else:
|
|
28
|
+
for key, value in task.items():
|
|
29
|
+
typer.echo(f"{key}: {value}")
|
|
30
|
+
except httpx.HTTPStatusError as e:
|
|
31
|
+
print_api_error(e, "get task")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
except httpx.RequestError as e:
|
|
34
|
+
print_api_error(e, "get task")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print_api_error(e, "get task")
|
|
38
|
+
raise typer.Exit(1)
|