cu-cli 0.1.0b1__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.
- cu_cli/__init__.py +17 -0
- cu_cli/__main__.py +11 -0
- cu_cli/apiversion.py +124 -0
- cu_cli/cli.py +138 -0
- cu_cli/client.py +138 -0
- cu_cli/commands/__init__.py +4 -0
- cu_cli/commands/_command_spec.py +94 -0
- cu_cli/commands/_help.py +33 -0
- cu_cli/commands/_infra_models.py +184 -0
- cu_cli/commands/_infra_wizard.py +630 -0
- cu_cli/commands/_model_setup.py +46 -0
- cu_cli/commands/_options.py +112 -0
- cu_cli/commands/analyze.py +631 -0
- cu_cli/commands/analyzer.py +1462 -0
- cu_cli/commands/defaults.py +172 -0
- cu_cli/commands/doctor.py +166 -0
- cu_cli/commands/env_var.py +67 -0
- cu_cli/commands/infra.py +302 -0
- cu_cli/commands/profile_cmd.py +525 -0
- cu_cli/commands/upgrade.py +120 -0
- cu_cli/core/__init__.py +17 -0
- cu_cli/core/analyze.py +44 -0
- cu_cli/core/analyzers.py +30 -0
- cu_cli/core/azure_resources.py +486 -0
- cu_cli/core/defaults.py +18 -0
- cu_cli/core/doctor.py +42 -0
- cu_cli/core/foundry.py +68 -0
- cu_cli/core/infra_models.py +367 -0
- cu_cli/core/inputs.py +209 -0
- cu_cli/core/schema.py +24 -0
- cu_cli/errors.py +174 -0
- cu_cli/exit_codes.py +20 -0
- cu_cli/modality.py +24 -0
- cu_cli/output.py +179 -0
- cu_cli/profile.py +30 -0
- cu_cli/py.typed +0 -0
- cu_cli/resources/__init__.py +4 -0
- cu_cli/resources/azd_template/README.md +187 -0
- cu_cli/resources/azd_template/azure.yaml +27 -0
- cu_cli/resources/azd_template/hooks/postprovision.ps1 +320 -0
- cu_cli/resources/azd_template/hooks/postprovision.sh +299 -0
- cu_cli/resources/azd_template/infra/main.bicep +115 -0
- cu_cli/resources/azd_template/infra/main.parameters.json +30 -0
- cu_cli/resources/azd_template/infra/models.json +1 -0
- cu_cli/resources/azd_template/infra/modules/foundry.bicep +122 -0
- cu_cli/schema_validate.py +28 -0
- cu_cli/spec_validate.py +18 -0
- cu_cli/telemetry.py +42 -0
- cu_cli/update_check.py +154 -0
- cu_cli/update_provider.py +92 -0
- cu_cli/windows_self_upgrade.py +245 -0
- cu_cli-0.1.0b1.dist-info/METADATA +345 -0
- cu_cli-0.1.0b1.dist-info/RECORD +56 -0
- cu_cli-0.1.0b1.dist-info/WHEEL +5 -0
- cu_cli-0.1.0b1.dist-info/entry_points.txt +3 -0
- cu_cli-0.1.0b1.dist-info/top_level.txt +1 -0
cu_cli/commands/infra.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""``cu infra generate`` — generate an azd template for CU infrastructure."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import rich_click as click
|
|
16
|
+
|
|
17
|
+
from ..apiversion import API_VERSION_HELP, DEFAULT_API_VERSION, ensure_supported
|
|
18
|
+
from ..core.foundry import endpoint_host, host_label, normalize_foundry_endpoint
|
|
19
|
+
from ..errors import CuCliError, friendly_errors
|
|
20
|
+
from ..exit_codes import VALIDATION_FAILURE
|
|
21
|
+
from ..output import console
|
|
22
|
+
from ._help import common_commands
|
|
23
|
+
from ._infra_wizard import _validate_azd_environment_name, run_wizard
|
|
24
|
+
|
|
25
|
+
AZURE_SIGNUP_URL = "https://azure.microsoft.com/free/"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class AzureAccount:
|
|
30
|
+
subscription_id: str
|
|
31
|
+
subscription_name: str
|
|
32
|
+
tenant_id: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _check_az_subscription(subscription: str | None = None) -> AzureAccount:
|
|
36
|
+
az = shutil.which("az")
|
|
37
|
+
if not az:
|
|
38
|
+
raise CuCliError(
|
|
39
|
+
"Provisioning requires Azure CLI (`az`), which was not found on PATH.",
|
|
40
|
+
hint=f"install it (https://aka.ms/azcli), sign up at {AZURE_SIGNUP_URL}, "
|
|
41
|
+
"then rerun `cu infra generate`.",
|
|
42
|
+
)
|
|
43
|
+
command = [az, "account", "show"]
|
|
44
|
+
if subscription:
|
|
45
|
+
command.extend(["--subscription", subscription])
|
|
46
|
+
command.extend(["--output", "json"])
|
|
47
|
+
try:
|
|
48
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
49
|
+
except OSError as exc:
|
|
50
|
+
raise CuCliError(f"could not run `az account show`: {exc}") from exc
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
target = f" '{subscription}'" if subscription else ""
|
|
53
|
+
raise CuCliError(
|
|
54
|
+
f"Azure subscription{target} is not accessible.",
|
|
55
|
+
hint=f"run `az login`, select a subscription, or sign up at "
|
|
56
|
+
f"{AZURE_SIGNUP_URL}, then rerun `cu infra generate`.",
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
payload = json.loads(result.stdout or "")
|
|
60
|
+
except json.JSONDecodeError as exc:
|
|
61
|
+
raise CuCliError("`az account show` returned invalid JSON.") from exc
|
|
62
|
+
if not isinstance(payload, dict):
|
|
63
|
+
raise CuCliError("`az account show` returned an invalid account record.")
|
|
64
|
+
subscription_id = str(payload.get("id") or "").strip()
|
|
65
|
+
subscription_name = str(payload.get("name") or "").strip()
|
|
66
|
+
tenant_id = str(payload.get("tenantId") or "").strip()
|
|
67
|
+
if not subscription_id or not subscription_name or not tenant_id:
|
|
68
|
+
raise CuCliError(
|
|
69
|
+
"`az account show` did not return subscription id, name, and tenant id."
|
|
70
|
+
)
|
|
71
|
+
return AzureAccount(subscription_id, subscription_name, tenant_id)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_models(value: str | None) -> list[str] | None:
|
|
75
|
+
if value is None:
|
|
76
|
+
return None
|
|
77
|
+
parts = [part.strip() for part in value.split(",")]
|
|
78
|
+
if any(not part for part in parts):
|
|
79
|
+
raise CuCliError(
|
|
80
|
+
"--models requires 'recommended', 'none', or one or more "
|
|
81
|
+
"comma-separated model names; empty entries are not allowed.",
|
|
82
|
+
hint="use `--models none` to deploy no models, or omit `--models` "
|
|
83
|
+
"for the interactive picker.",
|
|
84
|
+
exit_code=VALIDATION_FAILURE,
|
|
85
|
+
)
|
|
86
|
+
special = {"none", "recommended"}
|
|
87
|
+
if len(parts) > 1 and special.intersection(parts):
|
|
88
|
+
raise CuCliError(
|
|
89
|
+
"'none' and 'recommended' must each be used alone with --models.",
|
|
90
|
+
hint="use one special value or provide only comma-separated model names.",
|
|
91
|
+
exit_code=VALIDATION_FAILURE,
|
|
92
|
+
)
|
|
93
|
+
return parts
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _resolve_existing_foundry_account(
|
|
97
|
+
foundry_endpoint: str,
|
|
98
|
+
subscription_id: str,
|
|
99
|
+
) -> tuple[str, str, str | None]:
|
|
100
|
+
az = shutil.which("az")
|
|
101
|
+
if not az:
|
|
102
|
+
raise CuCliError(
|
|
103
|
+
"resolving an existing Foundry endpoint requires Azure CLI (`az`)."
|
|
104
|
+
)
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
[
|
|
107
|
+
az,
|
|
108
|
+
"cognitiveservices",
|
|
109
|
+
"account",
|
|
110
|
+
"list",
|
|
111
|
+
"--subscription",
|
|
112
|
+
subscription_id,
|
|
113
|
+
"--output",
|
|
114
|
+
"json",
|
|
115
|
+
],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
if result.returncode != 0:
|
|
120
|
+
raise CuCliError(
|
|
121
|
+
"could not list Cognitive Services accounts to resolve the endpoint.",
|
|
122
|
+
hint=(result.stderr or result.stdout).strip()
|
|
123
|
+
or "run `az account show` and retry.",
|
|
124
|
+
)
|
|
125
|
+
try:
|
|
126
|
+
accounts = json.loads(result.stdout or "[]")
|
|
127
|
+
except json.JSONDecodeError as exc:
|
|
128
|
+
raise CuCliError(
|
|
129
|
+
"Azure CLI returned invalid JSON while resolving the Foundry endpoint."
|
|
130
|
+
) from exc
|
|
131
|
+
target = foundry_endpoint.rstrip("/").lower()
|
|
132
|
+
target_host = endpoint_host(foundry_endpoint)
|
|
133
|
+
target_label = host_label(target_host)
|
|
134
|
+
for account in accounts:
|
|
135
|
+
if not isinstance(account, dict):
|
|
136
|
+
continue
|
|
137
|
+
properties_value = account.get("properties")
|
|
138
|
+
properties = (
|
|
139
|
+
properties_value if isinstance(properties_value, dict) else {}
|
|
140
|
+
)
|
|
141
|
+
account_endpoint = str(
|
|
142
|
+
properties.get("endpoint") or account.get("endpoint") or ""
|
|
143
|
+
)
|
|
144
|
+
name = str(account.get("name") or "").strip()
|
|
145
|
+
resource_group = str(account.get("resourceGroup") or "").strip()
|
|
146
|
+
if not name or not resource_group:
|
|
147
|
+
continue
|
|
148
|
+
exact_match = account_endpoint.rstrip("/").lower() == target
|
|
149
|
+
host_match = False
|
|
150
|
+
if target_host.endswith(".services.ai.azure.com"):
|
|
151
|
+
candidates = {name.lower()}
|
|
152
|
+
custom_subdomain = str(
|
|
153
|
+
properties.get("customSubDomainName") or ""
|
|
154
|
+
).strip().lower()
|
|
155
|
+
if custom_subdomain:
|
|
156
|
+
candidates.add(custom_subdomain)
|
|
157
|
+
properties_host = endpoint_host(
|
|
158
|
+
str(properties.get("endpoint") or "")
|
|
159
|
+
)
|
|
160
|
+
if properties_host:
|
|
161
|
+
candidates.add(host_label(properties_host))
|
|
162
|
+
host_match = bool(target_label and target_label in candidates)
|
|
163
|
+
if exact_match or host_match:
|
|
164
|
+
location = str(account.get("location") or "").strip()
|
|
165
|
+
return name, resource_group, location or None
|
|
166
|
+
raise CuCliError(
|
|
167
|
+
f"no Microsoft Foundry resource matched endpoint '{foundry_endpoint}'.",
|
|
168
|
+
hint="verify the endpoint, subscription, and your access with Azure CLI.",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@click.group(
|
|
173
|
+
"infra",
|
|
174
|
+
help=(
|
|
175
|
+
"Generate infrastructure-as-code used to provision and configure "
|
|
176
|
+
"Content Understanding resources."
|
|
177
|
+
),
|
|
178
|
+
epilog=common_commands(
|
|
179
|
+
(
|
|
180
|
+
"cu infra generate",
|
|
181
|
+
"Generate an azd/Bicep project. Run azd up to provision Azure resources.",
|
|
182
|
+
),
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
def infra_group() -> None:
|
|
186
|
+
"""Infrastructure-as-code generation commands."""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@infra_group.command(
|
|
190
|
+
"generate",
|
|
191
|
+
help=(
|
|
192
|
+
"Generate an azd/Bicep project used to provision a Microsoft Foundry "
|
|
193
|
+
"resource and configure Content Understanding. This command writes files "
|
|
194
|
+
"only; run `azd up` to provision Azure resources."
|
|
195
|
+
),
|
|
196
|
+
epilog=common_commands(
|
|
197
|
+
(
|
|
198
|
+
"cu infra generate",
|
|
199
|
+
"Generate a project for a new resource and optional model deployments. "
|
|
200
|
+
"Run azd up to provision it.",
|
|
201
|
+
),
|
|
202
|
+
(
|
|
203
|
+
"cu infra generate --foundry-endpoint URL",
|
|
204
|
+
"Generate a project for optional model deployments and defaults on an "
|
|
205
|
+
"existing resource. Run azd up to apply it.",
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
@click.option(
|
|
210
|
+
"-d",
|
|
211
|
+
"--output-dir",
|
|
212
|
+
type=click.Path(path_type=Path, file_okay=False),
|
|
213
|
+
default=Path("provision"),
|
|
214
|
+
show_default=True,
|
|
215
|
+
help="Directory where the azd template is generated.",
|
|
216
|
+
)
|
|
217
|
+
@click.option("-e", "--environment", default=None, help="azd environment name.")
|
|
218
|
+
@click.option("-l", "--location", default=None, help="Azure region.")
|
|
219
|
+
@click.option("--subscription", default=None, help="Azure subscription name or ID.")
|
|
220
|
+
@click.option("--api-version", default=None, help=API_VERSION_HELP)
|
|
221
|
+
@click.option(
|
|
222
|
+
"--models",
|
|
223
|
+
default=None,
|
|
224
|
+
help=(
|
|
225
|
+
"Comma-separated model names, 'recommended', or 'none'; omit for an interactive "
|
|
226
|
+
"picker. Explicit names are validated against the live model catalog during azd up."
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
@click.option(
|
|
230
|
+
"--foundry-endpoint",
|
|
231
|
+
default=None,
|
|
232
|
+
help="Existing Microsoft Foundry resource endpoint; only selected models are deployed.",
|
|
233
|
+
)
|
|
234
|
+
@click.option(
|
|
235
|
+
"--foundry-prefix",
|
|
236
|
+
default=None,
|
|
237
|
+
help="Prefix for a new globally unique Microsoft Foundry resource name.",
|
|
238
|
+
)
|
|
239
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing generated template.")
|
|
240
|
+
@friendly_errors
|
|
241
|
+
def cmd_infra_generate(
|
|
242
|
+
output_dir: Path,
|
|
243
|
+
environment: str | None,
|
|
244
|
+
location: str | None,
|
|
245
|
+
subscription: str | None,
|
|
246
|
+
api_version: str | None,
|
|
247
|
+
models: str | None,
|
|
248
|
+
foundry_endpoint: str | None,
|
|
249
|
+
foundry_prefix: str | None,
|
|
250
|
+
force: bool,
|
|
251
|
+
) -> None:
|
|
252
|
+
selected_models = _parse_models(models)
|
|
253
|
+
version = ensure_supported(api_version or DEFAULT_API_VERSION)
|
|
254
|
+
if environment is not None:
|
|
255
|
+
environment = _validate_azd_environment_name(environment)
|
|
256
|
+
if foundry_endpoint and foundry_prefix:
|
|
257
|
+
raise CuCliError(
|
|
258
|
+
"--foundry-prefix cannot be used with --foundry-endpoint.",
|
|
259
|
+
hint="omit the prefix when targeting an existing Foundry resource.",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
normalized_endpoint = (
|
|
263
|
+
normalize_foundry_endpoint(foundry_endpoint)
|
|
264
|
+
if foundry_endpoint
|
|
265
|
+
else None
|
|
266
|
+
)
|
|
267
|
+
account = _check_az_subscription(subscription)
|
|
268
|
+
existing_resource_group: str | None = None
|
|
269
|
+
if normalized_endpoint:
|
|
270
|
+
_, existing_resource_group, existing_location = (
|
|
271
|
+
_resolve_existing_foundry_account(
|
|
272
|
+
normalized_endpoint,
|
|
273
|
+
account.subscription_id,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
if location is None:
|
|
277
|
+
location = existing_location
|
|
278
|
+
|
|
279
|
+
target = output_dir.resolve()
|
|
280
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
console.print(f"\n[bold]cu infra generate[/bold] -> [cyan]{target}[/cyan]")
|
|
282
|
+
console.print(
|
|
283
|
+
f" [dim]Azure subscription: {account.subscription_name} "
|
|
284
|
+
f"({account.subscription_id})[/dim]"
|
|
285
|
+
)
|
|
286
|
+
run_wizard(
|
|
287
|
+
target,
|
|
288
|
+
interactive=sys.stdin.isatty(),
|
|
289
|
+
already_opted_in=True,
|
|
290
|
+
env=environment,
|
|
291
|
+
location=location,
|
|
292
|
+
api_version=version,
|
|
293
|
+
subscription_id=account.subscription_id,
|
|
294
|
+
subscription_name=account.subscription_name,
|
|
295
|
+
tenant_id=account.tenant_id,
|
|
296
|
+
foundry_account_prefix=foundry_prefix,
|
|
297
|
+
foundry_endpoint=normalized_endpoint,
|
|
298
|
+
foundry_resource_group=existing_resource_group,
|
|
299
|
+
models=selected_models,
|
|
300
|
+
assign_roles=None,
|
|
301
|
+
force=force,
|
|
302
|
+
)
|