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
bt_cli/core/config.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Multi-product configuration management for BeyondTrust CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from .config_file import get_layered_config, get_secret_from_keyring
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ProductConfig:
|
|
14
|
+
"""Base configuration shared by all products."""
|
|
15
|
+
|
|
16
|
+
api_url: str
|
|
17
|
+
verify_ssl: bool = True
|
|
18
|
+
timeout: float = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class PWSConfig(ProductConfig):
|
|
23
|
+
"""Password Safe configuration.
|
|
24
|
+
|
|
25
|
+
Supports two authentication methods:
|
|
26
|
+
- API Key: Uses PS-Auth header format
|
|
27
|
+
- OAuth: Uses client_credentials grant flow
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
api_key: Optional[str] = None
|
|
31
|
+
client_id: Optional[str] = None
|
|
32
|
+
client_secret: Optional[str] = None
|
|
33
|
+
run_as: Optional[str] = None
|
|
34
|
+
api_version: str = "3.1"
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def auth_method(self) -> str:
|
|
38
|
+
"""Determine which auth method to use."""
|
|
39
|
+
if self.api_key:
|
|
40
|
+
return "api_key"
|
|
41
|
+
if self.client_id and self.client_secret:
|
|
42
|
+
return "oauth"
|
|
43
|
+
raise ValueError("PWS requires either API key or OAuth credentials")
|
|
44
|
+
|
|
45
|
+
def validate(self) -> None:
|
|
46
|
+
"""Validate configuration."""
|
|
47
|
+
if not self.api_url:
|
|
48
|
+
raise ValueError("BT_PWS_API_URL is required")
|
|
49
|
+
if not self.api_key and not (self.client_id and self.client_secret):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"PWS requires either BT_PWS_API_KEY or both BT_PWS_CLIENT_ID and BT_PWS_CLIENT_SECRET"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class EntitleConfig(ProductConfig):
|
|
57
|
+
"""Entitle configuration.
|
|
58
|
+
|
|
59
|
+
Uses simple Bearer token authentication.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
api_key: str = ""
|
|
63
|
+
|
|
64
|
+
def validate(self) -> None:
|
|
65
|
+
"""Validate configuration."""
|
|
66
|
+
if not self.api_key:
|
|
67
|
+
raise ValueError("BT_ENTITLE_API_KEY is required")
|
|
68
|
+
if not self.api_url:
|
|
69
|
+
raise ValueError("BT_ENTITLE_API_URL is required")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PRAConfig(ProductConfig):
|
|
74
|
+
"""Privileged Remote Access configuration.
|
|
75
|
+
|
|
76
|
+
Uses OAuth 2.0 client credentials authentication.
|
|
77
|
+
API base path: /api/config/v1
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
client_id: str = ""
|
|
81
|
+
client_secret: str = ""
|
|
82
|
+
|
|
83
|
+
def validate(self) -> None:
|
|
84
|
+
"""Validate configuration."""
|
|
85
|
+
if not self.api_url:
|
|
86
|
+
raise ValueError("BT_PRA_API_URL is required")
|
|
87
|
+
if not self.client_id or not self.client_secret:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"PRA requires both BT_PRA_CLIENT_ID and BT_PRA_CLIENT_SECRET"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class EPMWConfig(ProductConfig):
|
|
95
|
+
"""EPM Windows configuration.
|
|
96
|
+
|
|
97
|
+
Uses OAuth 2.0 client credentials authentication.
|
|
98
|
+
API base path: /management-api/v3
|
|
99
|
+
Token endpoint: /oauth/token
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
client_id: str = ""
|
|
103
|
+
client_secret: str = ""
|
|
104
|
+
|
|
105
|
+
def validate(self) -> None:
|
|
106
|
+
"""Validate configuration."""
|
|
107
|
+
if not self.api_url:
|
|
108
|
+
raise ValueError("BT_EPM_API_URL is required")
|
|
109
|
+
if not self.client_id or not self.client_secret:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"EPMW requires both BT_EPM_CLIENT_ID and BT_EPM_CLIENT_SECRET"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_bool(value: Optional[str], default: bool = True) -> bool:
|
|
116
|
+
"""Parse boolean from environment variable."""
|
|
117
|
+
if value is None:
|
|
118
|
+
return default
|
|
119
|
+
return value.lower() not in ("false", "0", "no", "off")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_float(value: Optional[str], default: float, min_val: float = 0.1, max_val: float = 600.0) -> float:
|
|
123
|
+
"""Parse float from environment variable with range validation.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
value: String value to parse
|
|
127
|
+
default: Default if None or invalid
|
|
128
|
+
min_val: Minimum allowed value (default 0.1 seconds)
|
|
129
|
+
max_val: Maximum allowed value (default 600 seconds / 10 minutes)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Float value within valid range, or default if invalid
|
|
133
|
+
"""
|
|
134
|
+
if value is None:
|
|
135
|
+
return default
|
|
136
|
+
try:
|
|
137
|
+
result = float(value)
|
|
138
|
+
# Validate range - use default if out of bounds
|
|
139
|
+
if result < min_val or result > max_val:
|
|
140
|
+
return default
|
|
141
|
+
return result
|
|
142
|
+
except ValueError:
|
|
143
|
+
return default
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _resolve_value(value: Any) -> Any:
|
|
147
|
+
"""Resolve keyring references in config values.
|
|
148
|
+
|
|
149
|
+
If value is a string starting with 'keyring://', fetch from keyring.
|
|
150
|
+
"""
|
|
151
|
+
if isinstance(value, str) and value.startswith("keyring://"):
|
|
152
|
+
# Format: keyring://service/key
|
|
153
|
+
parts = value[len("keyring://"):].split("/", 1)
|
|
154
|
+
if len(parts) == 2:
|
|
155
|
+
service, key = parts
|
|
156
|
+
resolved = get_secret_from_keyring(service, key)
|
|
157
|
+
if resolved:
|
|
158
|
+
return resolved
|
|
159
|
+
# Return None if keyring lookup fails
|
|
160
|
+
return None
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _to_bool(value: Any) -> bool:
|
|
165
|
+
"""Convert value to boolean."""
|
|
166
|
+
if isinstance(value, bool):
|
|
167
|
+
return value
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
return value.lower() not in ("false", "0", "no", "off", "")
|
|
170
|
+
return bool(value)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _to_float(value: Any, default: float = 30.0, min_val: float = 0.1, max_val: float = 600.0) -> float:
|
|
174
|
+
"""Convert value to float with range validation.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
value: Value to convert
|
|
178
|
+
default: Default if None or invalid
|
|
179
|
+
min_val: Minimum allowed value (default 0.1 seconds)
|
|
180
|
+
max_val: Maximum allowed value (default 600 seconds / 10 minutes)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Float value within valid range, or default if invalid
|
|
184
|
+
"""
|
|
185
|
+
if value is None:
|
|
186
|
+
return default
|
|
187
|
+
try:
|
|
188
|
+
result = float(value)
|
|
189
|
+
# Validate range - use default if out of bounds
|
|
190
|
+
if result < min_val or result > max_val:
|
|
191
|
+
return default
|
|
192
|
+
return result
|
|
193
|
+
except (ValueError, TypeError):
|
|
194
|
+
return default
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _get_profile() -> Optional[str]:
|
|
198
|
+
"""Get the active profile from CLI or environment."""
|
|
199
|
+
# Try to get from CLI module
|
|
200
|
+
try:
|
|
201
|
+
from ..cli import get_active_profile
|
|
202
|
+
profile = get_active_profile()
|
|
203
|
+
if profile:
|
|
204
|
+
return profile
|
|
205
|
+
except ImportError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Fall back to environment variable
|
|
209
|
+
return os.getenv("BT_PROFILE")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def load_pws_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> PWSConfig:
|
|
213
|
+
"""Load Password Safe configuration.
|
|
214
|
+
|
|
215
|
+
Configuration sources (in order of precedence):
|
|
216
|
+
1. Environment variables
|
|
217
|
+
2. Config file (~/.bt-cli/config.yaml)
|
|
218
|
+
|
|
219
|
+
Environment variables:
|
|
220
|
+
BT_PWS_API_URL - API endpoint URL (required)
|
|
221
|
+
BT_PWS_API_KEY - API key for PS-Auth authentication
|
|
222
|
+
BT_PWS_CLIENT_ID - OAuth client ID
|
|
223
|
+
BT_PWS_CLIENT_SECRET - OAuth client secret
|
|
224
|
+
BT_PWS_VERIFY_SSL - SSL verification (default: true)
|
|
225
|
+
BT_PWS_TIMEOUT - Request timeout in seconds (default: 30)
|
|
226
|
+
BT_PWS_RUN_AS - Username for impersonation
|
|
227
|
+
BT_PWS_API_VERSION - API version (default: 3.1)
|
|
228
|
+
"""
|
|
229
|
+
if env_file:
|
|
230
|
+
load_dotenv(env_file)
|
|
231
|
+
else:
|
|
232
|
+
load_dotenv()
|
|
233
|
+
|
|
234
|
+
# Get layered config (file + env vars)
|
|
235
|
+
profile = profile or _get_profile()
|
|
236
|
+
layered = get_layered_config("pws", profile)
|
|
237
|
+
|
|
238
|
+
# Resolve any keyring references
|
|
239
|
+
for key in ["api_key", "client_secret"]:
|
|
240
|
+
if key in layered:
|
|
241
|
+
layered[key] = _resolve_value(layered[key])
|
|
242
|
+
|
|
243
|
+
config = PWSConfig(
|
|
244
|
+
api_url=layered.get("api_url") or os.getenv("BT_PWS_API_URL", ""),
|
|
245
|
+
api_key=layered.get("api_key") or os.getenv("BT_PWS_API_KEY"),
|
|
246
|
+
client_id=layered.get("client_id") or os.getenv("BT_PWS_CLIENT_ID"),
|
|
247
|
+
client_secret=layered.get("client_secret") or os.getenv("BT_PWS_CLIENT_SECRET"),
|
|
248
|
+
verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_PWS_VERIFY_SSL")),
|
|
249
|
+
timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_PWS_TIMEOUT"), 30.0),
|
|
250
|
+
run_as=layered.get("run_as") or os.getenv("BT_PWS_RUN_AS"),
|
|
251
|
+
api_version=layered.get("api_version") or os.getenv("BT_PWS_API_VERSION", "3.1"),
|
|
252
|
+
)
|
|
253
|
+
config.validate()
|
|
254
|
+
return config
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def load_entitle_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> EntitleConfig:
|
|
258
|
+
"""Load Entitle configuration.
|
|
259
|
+
|
|
260
|
+
Configuration sources (in order of precedence):
|
|
261
|
+
1. Environment variables
|
|
262
|
+
2. Config file (~/.bt-cli/config.yaml)
|
|
263
|
+
|
|
264
|
+
Environment variables:
|
|
265
|
+
BT_ENTITLE_API_URL - API endpoint URL (default: https://api.us.entitle.io)
|
|
266
|
+
BT_ENTITLE_API_KEY - API key for Bearer authentication (required)
|
|
267
|
+
BT_ENTITLE_VERIFY_SSL - SSL verification (default: true)
|
|
268
|
+
BT_ENTITLE_TIMEOUT - Request timeout in seconds (default: 30)
|
|
269
|
+
"""
|
|
270
|
+
if env_file:
|
|
271
|
+
load_dotenv(env_file)
|
|
272
|
+
else:
|
|
273
|
+
load_dotenv()
|
|
274
|
+
|
|
275
|
+
# Get layered config (file + env vars)
|
|
276
|
+
profile = profile or _get_profile()
|
|
277
|
+
layered = get_layered_config("entitle", profile)
|
|
278
|
+
|
|
279
|
+
# Resolve any keyring references
|
|
280
|
+
if "api_key" in layered:
|
|
281
|
+
layered["api_key"] = _resolve_value(layered["api_key"])
|
|
282
|
+
|
|
283
|
+
config = EntitleConfig(
|
|
284
|
+
api_url=layered.get("api_url") or os.getenv("BT_ENTITLE_API_URL", "https://api.us.entitle.io"),
|
|
285
|
+
api_key=layered.get("api_key") or os.getenv("BT_ENTITLE_API_KEY", ""),
|
|
286
|
+
verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_ENTITLE_VERIFY_SSL")),
|
|
287
|
+
timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_ENTITLE_TIMEOUT"), 30.0),
|
|
288
|
+
)
|
|
289
|
+
config.validate()
|
|
290
|
+
return config
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def load_pra_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> PRAConfig:
|
|
294
|
+
"""Load PRA configuration.
|
|
295
|
+
|
|
296
|
+
Configuration sources (in order of precedence):
|
|
297
|
+
1. Environment variables
|
|
298
|
+
2. Config file (~/.bt-cli/config.yaml)
|
|
299
|
+
|
|
300
|
+
Environment variables:
|
|
301
|
+
BT_PRA_API_URL - API endpoint URL (required, e.g., https://host.beyondtrustcloud.com)
|
|
302
|
+
BT_PRA_CLIENT_ID - OAuth client ID (required)
|
|
303
|
+
BT_PRA_CLIENT_SECRET - OAuth client secret (required)
|
|
304
|
+
BT_PRA_VERIFY_SSL - SSL verification (default: true)
|
|
305
|
+
BT_PRA_TIMEOUT - Request timeout in seconds (default: 30)
|
|
306
|
+
"""
|
|
307
|
+
if env_file:
|
|
308
|
+
load_dotenv(env_file)
|
|
309
|
+
else:
|
|
310
|
+
load_dotenv()
|
|
311
|
+
|
|
312
|
+
# Get layered config (file + env vars)
|
|
313
|
+
profile = profile or _get_profile()
|
|
314
|
+
layered = get_layered_config("pra", profile)
|
|
315
|
+
|
|
316
|
+
# Resolve any keyring references
|
|
317
|
+
if "client_secret" in layered:
|
|
318
|
+
layered["client_secret"] = _resolve_value(layered["client_secret"])
|
|
319
|
+
|
|
320
|
+
config = PRAConfig(
|
|
321
|
+
api_url=layered.get("api_url") or os.getenv("BT_PRA_API_URL", ""),
|
|
322
|
+
client_id=layered.get("client_id") or os.getenv("BT_PRA_CLIENT_ID", ""),
|
|
323
|
+
client_secret=layered.get("client_secret") or os.getenv("BT_PRA_CLIENT_SECRET", ""),
|
|
324
|
+
verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_PRA_VERIFY_SSL")),
|
|
325
|
+
timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_PRA_TIMEOUT"), 30.0),
|
|
326
|
+
)
|
|
327
|
+
config.validate()
|
|
328
|
+
return config
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def load_epmw_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> EPMWConfig:
|
|
332
|
+
"""Load EPM Windows configuration.
|
|
333
|
+
|
|
334
|
+
Configuration sources (in order of precedence):
|
|
335
|
+
1. Environment variables
|
|
336
|
+
2. Config file (~/.bt-cli/config.yaml)
|
|
337
|
+
|
|
338
|
+
Environment variables:
|
|
339
|
+
BT_EPM_API_URL - API endpoint URL (required, e.g., https://host-services.epm.bt3ng.com)
|
|
340
|
+
BT_EPM_CLIENT_ID - OAuth client ID (required)
|
|
341
|
+
BT_EPM_CLIENT_SECRET - OAuth client secret (required)
|
|
342
|
+
BT_EPM_VERIFY_SSL - SSL verification (default: true)
|
|
343
|
+
BT_EPM_TIMEOUT - Request timeout in seconds (default: 30)
|
|
344
|
+
"""
|
|
345
|
+
if env_file:
|
|
346
|
+
load_dotenv(env_file)
|
|
347
|
+
else:
|
|
348
|
+
load_dotenv()
|
|
349
|
+
|
|
350
|
+
# Get layered config (file + env vars)
|
|
351
|
+
profile = profile or _get_profile()
|
|
352
|
+
layered = get_layered_config("epmw", profile)
|
|
353
|
+
|
|
354
|
+
# Resolve any keyring references
|
|
355
|
+
if "client_secret" in layered:
|
|
356
|
+
layered["client_secret"] = _resolve_value(layered["client_secret"])
|
|
357
|
+
|
|
358
|
+
config = EPMWConfig(
|
|
359
|
+
api_url=layered.get("api_url") or os.getenv("BT_EPM_API_URL", ""),
|
|
360
|
+
client_id=layered.get("client_id") or os.getenv("BT_EPM_CLIENT_ID", ""),
|
|
361
|
+
client_secret=layered.get("client_secret") or os.getenv("BT_EPM_CLIENT_SECRET", ""),
|
|
362
|
+
verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_EPM_VERIFY_SSL")),
|
|
363
|
+
timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_EPM_TIMEOUT"), 30.0),
|
|
364
|
+
)
|
|
365
|
+
config.validate()
|
|
366
|
+
return config
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def load_config(product: str, env_file: Optional[str] = None) -> ProductConfig:
|
|
370
|
+
"""Load configuration for a specific product.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
product: Product name ('pws', 'entitle', 'pra', 'epmw')
|
|
374
|
+
env_file: Optional path to .env file
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Product-specific configuration object
|
|
378
|
+
|
|
379
|
+
Raises:
|
|
380
|
+
ValueError: If product is unknown or configuration is invalid
|
|
381
|
+
"""
|
|
382
|
+
loaders = {
|
|
383
|
+
"pws": load_pws_config,
|
|
384
|
+
"entitle": load_entitle_config,
|
|
385
|
+
"pra": load_pra_config,
|
|
386
|
+
"epmw": load_epmw_config,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
loader = loaders.get(product.lower())
|
|
390
|
+
if not loader:
|
|
391
|
+
raise ValueError(f"Unknown product: {product}. Available: {list(loaders.keys())}")
|
|
392
|
+
|
|
393
|
+
return loader(env_file)
|