fdai-deployment-cli 0.1.2__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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
"""Supervise signed-kit Azure Foundation without GitHub services or a source checkout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from fdai_service_contracts.product_profile import ProductAddOn
|
|
19
|
+
|
|
20
|
+
from fdai_deployment_cli.aks_preflight import inspect_aks_target
|
|
21
|
+
from fdai_deployment_cli.application_state_adoption import (
|
|
22
|
+
ApplicationStateAdoption,
|
|
23
|
+
stage_application_state_adoption,
|
|
24
|
+
)
|
|
25
|
+
from fdai_deployment_cli.azure_naming import selected_azure_region_short_name
|
|
26
|
+
from fdai_deployment_cli.catalog_review_profile import CatalogReviewDeploymentProfile
|
|
27
|
+
from fdai_deployment_cli.control_package import verify_control_package
|
|
28
|
+
from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
|
|
29
|
+
from fdai_deployment_cli.deployment_kit import DeploymentKit, acquire_deployment_kit
|
|
30
|
+
from fdai_deployment_cli.deployment_progress import begin_stage, progress_detail, terminal_output
|
|
31
|
+
from fdai_deployment_cli.foundation_failure import foundation_failure_summary
|
|
32
|
+
from fdai_deployment_cli.foundation_output import foundation_output
|
|
33
|
+
from fdai_deployment_cli.foundation_process import run_foundation_process
|
|
34
|
+
from fdai_deployment_cli.operational_evidence_verifier_input import (
|
|
35
|
+
OperationalEvidenceVerifierDeploymentInput,
|
|
36
|
+
)
|
|
37
|
+
from fdai_deployment_cli.private_output import read_private_bytes
|
|
38
|
+
from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
|
|
39
|
+
from fdai_deployment_cli.standalone_application_completion import complete_application
|
|
40
|
+
from fdai_deployment_cli.standalone_foundation_adoption import (
|
|
41
|
+
deploy_with_adopted_foundation,
|
|
42
|
+
)
|
|
43
|
+
from fdai_deployment_cli.standalone_foundation_transition import (
|
|
44
|
+
default_transition_runner,
|
|
45
|
+
run_foundation_transition,
|
|
46
|
+
)
|
|
47
|
+
from fdai_deployment_cli.standalone_status import current_status, prior_attempt
|
|
48
|
+
from fdai_deployment_cli.target import (
|
|
49
|
+
azure_target_selection_arguments,
|
|
50
|
+
require_selected_azure_target,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
|
|
54
|
+
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
|
55
|
+
_SAFE_ENVIRONMENT_KEYS = frozenset(
|
|
56
|
+
{
|
|
57
|
+
"AZURE_CONFIG_DIR",
|
|
58
|
+
"CURL_CA_BUNDLE",
|
|
59
|
+
"HOME",
|
|
60
|
+
"LANG",
|
|
61
|
+
"LC_ALL",
|
|
62
|
+
"LC_CTYPE",
|
|
63
|
+
"LOGNAME",
|
|
64
|
+
"PATH",
|
|
65
|
+
"REQUESTS_CA_BUNDLE",
|
|
66
|
+
"SSL_CERT_FILE",
|
|
67
|
+
"TERM",
|
|
68
|
+
"TMPDIR",
|
|
69
|
+
"TZ",
|
|
70
|
+
"USER",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class ActiveAzureTarget:
|
|
77
|
+
"""Authenticated Azure user target selected by the current CLI profile."""
|
|
78
|
+
|
|
79
|
+
subscription_id: str
|
|
80
|
+
tenant_id: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def deploy_azure_foundation(
|
|
84
|
+
*,
|
|
85
|
+
work_dir: Path,
|
|
86
|
+
offline_kit: Path | None,
|
|
87
|
+
online: bool = False,
|
|
88
|
+
online_url: str | None = None,
|
|
89
|
+
region: str,
|
|
90
|
+
monthly_cost_ceiling: int,
|
|
91
|
+
timeout_seconds: int,
|
|
92
|
+
trial_token: Path | None,
|
|
93
|
+
runtime_profile: RuntimeDeploymentProfile | None = None,
|
|
94
|
+
adopt_runner_image_receipt: Path | None = None,
|
|
95
|
+
adopt_application_state: Path | None = None,
|
|
96
|
+
adopt_application_recovery: Path | None = None,
|
|
97
|
+
adopt_resolved_models: Path | None = None,
|
|
98
|
+
adopt_foundation_directory: Path | None = None,
|
|
99
|
+
adopt_foundation_recovery_directory: Path | None = None,
|
|
100
|
+
catalog_review_profile: CatalogReviewDeploymentProfile | None = None,
|
|
101
|
+
operational_evidence_verifier_input: OperationalEvidenceVerifierDeploymentInput | None = None,
|
|
102
|
+
control_package: Path | None = None,
|
|
103
|
+
) -> dict[str, object]:
|
|
104
|
+
"""Advance one standalone deployment through verified application convergence.
|
|
105
|
+
|
|
106
|
+
``control_package`` optionally selects a signed deployment-control wheelhouse that replaces
|
|
107
|
+
only the managed-host CLI; the verified kit still supplies every runtime payload.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
deadline = DeploymentDeadline(timeout_seconds, clock=time.monotonic)
|
|
111
|
+
selected_runtime = runtime_profile or RuntimeDeploymentProfile.create(
|
|
112
|
+
runtime_platform="container-apps",
|
|
113
|
+
database_placement="postgres-flex",
|
|
114
|
+
)
|
|
115
|
+
begin_stage("azure")
|
|
116
|
+
progress_detail("Checking the active Azure CLI human identity")
|
|
117
|
+
target = active_azure_target()
|
|
118
|
+
deadline.remaining()
|
|
119
|
+
_create_or_validate_private_directory(work_dir)
|
|
120
|
+
kit_work = work_dir / "kit-work"
|
|
121
|
+
_create_or_validate_private_directory(kit_work)
|
|
122
|
+
begin_stage("kit")
|
|
123
|
+
kit = acquire_deployment_kit(
|
|
124
|
+
work_dir=kit_work,
|
|
125
|
+
online=online,
|
|
126
|
+
offline_kit=offline_kit,
|
|
127
|
+
online_url=online_url,
|
|
128
|
+
)
|
|
129
|
+
deadline.remaining()
|
|
130
|
+
control = None
|
|
131
|
+
if control_package is not None:
|
|
132
|
+
if online:
|
|
133
|
+
raise ValueError("a control package requires a local signed offline kit")
|
|
134
|
+
progress_detail("Verifying the signed deployment-control package")
|
|
135
|
+
control = verify_control_package(control_package)
|
|
136
|
+
adoption_inputs = (
|
|
137
|
+
adopt_application_state,
|
|
138
|
+
adopt_application_recovery,
|
|
139
|
+
adopt_resolved_models,
|
|
140
|
+
)
|
|
141
|
+
if any(value is not None for value in adoption_inputs) and not all(
|
|
142
|
+
value is not None for value in adoption_inputs
|
|
143
|
+
):
|
|
144
|
+
raise ValueError("recovered public deployment requires all three adoption inputs")
|
|
145
|
+
adoption: ApplicationStateAdoption | None = None
|
|
146
|
+
if all(value is not None for value in adoption_inputs):
|
|
147
|
+
assert adopt_application_state is not None
|
|
148
|
+
assert adopt_application_recovery is not None
|
|
149
|
+
assert adopt_resolved_models is not None
|
|
150
|
+
region_short = selected_azure_region_short_name(
|
|
151
|
+
region=region,
|
|
152
|
+
subscription_id=target.subscription_id,
|
|
153
|
+
environment="dev",
|
|
154
|
+
workload="fdai",
|
|
155
|
+
retained_variables=work_dir / "run" / "foundation-variables.json",
|
|
156
|
+
)
|
|
157
|
+
adoption = stage_application_state_adoption(
|
|
158
|
+
source_state=adopt_application_state,
|
|
159
|
+
recovery_receipt=adopt_application_recovery,
|
|
160
|
+
resolved_models=adopt_resolved_models,
|
|
161
|
+
output_directory=work_dir / "application-state-adoption",
|
|
162
|
+
subscription_id=target.subscription_id,
|
|
163
|
+
resource_group_name=f"rg-fdai-dev-{region_short}",
|
|
164
|
+
environment="dev",
|
|
165
|
+
region_short=region_short,
|
|
166
|
+
)
|
|
167
|
+
adopted = deploy_with_adopted_foundation(
|
|
168
|
+
foundation_directory=adopt_foundation_directory,
|
|
169
|
+
recovery_directory=adopt_foundation_recovery_directory,
|
|
170
|
+
adopt_runner_image_receipt=adopt_runner_image_receipt,
|
|
171
|
+
work_dir=work_dir,
|
|
172
|
+
kit=kit,
|
|
173
|
+
tenant_id=target.tenant_id,
|
|
174
|
+
subscription_id=target.subscription_id,
|
|
175
|
+
region=region,
|
|
176
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
177
|
+
deadline=deadline,
|
|
178
|
+
selected_runtime=selected_runtime,
|
|
179
|
+
trial_token=trial_token,
|
|
180
|
+
application_state_adoption=adoption,
|
|
181
|
+
catalog_review_profile=catalog_review_profile,
|
|
182
|
+
operational_evidence_verifier_input=operational_evidence_verifier_input,
|
|
183
|
+
current_operator_object_id=_current_operator_object_id,
|
|
184
|
+
control_package=control,
|
|
185
|
+
)
|
|
186
|
+
if adopted is not None:
|
|
187
|
+
return adopted
|
|
188
|
+
if (
|
|
189
|
+
selected_runtime.runtime_platform.value == "aks"
|
|
190
|
+
and adoption is None
|
|
191
|
+
and not (work_dir / "run" / "status.json").exists()
|
|
192
|
+
):
|
|
193
|
+
if _identity_add_on_selected(selected_runtime):
|
|
194
|
+
_require_unique_entra_display_names(
|
|
195
|
+
selected_runtime, kit.bundle_root / "scripts/deployment/azure"
|
|
196
|
+
)
|
|
197
|
+
_require_feasible_new_aks_target(selected_runtime, region=region, deadline=deadline)
|
|
198
|
+
begin_stage("discovery")
|
|
199
|
+
progress_detail("Discovering image, storage name, and non-overlapping networks")
|
|
200
|
+
scripts = kit.bundle_root / "scripts/deployment/azure"
|
|
201
|
+
if not scripts.is_dir() or scripts.is_symlink():
|
|
202
|
+
raise ValueError("verified deployment bundle is missing Azure orchestration")
|
|
203
|
+
create_runner_image = not online and adopt_runner_image_receipt is None
|
|
204
|
+
sys.path.insert(0, str(scripts))
|
|
205
|
+
try:
|
|
206
|
+
prepare = importlib.import_module("genesis_prepare")
|
|
207
|
+
prepared: Any = prepare.prepare_standalone_genesis(
|
|
208
|
+
deployment_kit=kit,
|
|
209
|
+
tenant_id=target.tenant_id,
|
|
210
|
+
subscription_id=target.subscription_id,
|
|
211
|
+
region=region,
|
|
212
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
213
|
+
connectivity="online" if online else "offline",
|
|
214
|
+
root=work_dir / "run",
|
|
215
|
+
create_runner_image=create_runner_image,
|
|
216
|
+
region_short=selected_azure_region_short_name(
|
|
217
|
+
region=region,
|
|
218
|
+
subscription_id=target.subscription_id,
|
|
219
|
+
environment="dev",
|
|
220
|
+
workload="fdai",
|
|
221
|
+
retained_variables=work_dir / "run" / "foundation-variables.json",
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
foundation_variables = prepared.variables
|
|
225
|
+
if adopt_runner_image_receipt is not None:
|
|
226
|
+
runner_image = importlib.import_module("genesis_runner_image_contract")
|
|
227
|
+
foundation_variables = prepared.root / "foundation-with-runner-image.json"
|
|
228
|
+
if foundation_variables.exists():
|
|
229
|
+
runner_image.verify_foundation_image_input(
|
|
230
|
+
source=prepared.variables,
|
|
231
|
+
image_receipt=adopt_runner_image_receipt,
|
|
232
|
+
profile_path=prepared.profile,
|
|
233
|
+
destination=foundation_variables,
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
runner_image.materialize_foundation_image_input(
|
|
237
|
+
source=prepared.variables,
|
|
238
|
+
image_receipt=adopt_runner_image_receipt,
|
|
239
|
+
profile_path=prepared.profile,
|
|
240
|
+
destination=foundation_variables,
|
|
241
|
+
)
|
|
242
|
+
finally:
|
|
243
|
+
sys.path.remove(str(scripts))
|
|
244
|
+
# An offline kit upgrade continues the retained Foundation under the revision that created
|
|
245
|
+
# it; the newly verified kit remains the application source.
|
|
246
|
+
foundation_source = str(getattr(prepared, "foundation_source_commit", "") or kit.source_commit)
|
|
247
|
+
lineage_continuation = foundation_source != kit.source_commit
|
|
248
|
+
source_evidence = json.dumps(
|
|
249
|
+
{
|
|
250
|
+
"source_commit": kit.source_commit,
|
|
251
|
+
"foundation_source_commit": foundation_source,
|
|
252
|
+
"kit_manifest_digest": kit.verification.manifest_digest,
|
|
253
|
+
"bundle_manifest_digest": kit.bundle_manifest_digest,
|
|
254
|
+
"runtime_release_digest": kit.runtime.digest,
|
|
255
|
+
},
|
|
256
|
+
sort_keys=True,
|
|
257
|
+
separators=(",", ":"),
|
|
258
|
+
)
|
|
259
|
+
approval = prepared.root / "current-foundation-approval.json"
|
|
260
|
+
status_path = prepared.root / "status.json"
|
|
261
|
+
begin_stage("foundation")
|
|
262
|
+
while True:
|
|
263
|
+
remaining = deadline.remaining()
|
|
264
|
+
if remaining < 1830:
|
|
265
|
+
raise TimeoutError("standalone Foundation deadline has insufficient remaining budget")
|
|
266
|
+
previous_attempt = prior_attempt(status_path)
|
|
267
|
+
stage_timeout = min(14_400, remaining - 30)
|
|
268
|
+
command = (
|
|
269
|
+
sys.executable,
|
|
270
|
+
str(scripts / "genesis_orchestrator.py"),
|
|
271
|
+
"--environment",
|
|
272
|
+
"dev",
|
|
273
|
+
"--region",
|
|
274
|
+
region,
|
|
275
|
+
"--apply",
|
|
276
|
+
"--allow-probe-resources",
|
|
277
|
+
"--source-commit",
|
|
278
|
+
foundation_source,
|
|
279
|
+
"--work-dir",
|
|
280
|
+
str(prepared.root),
|
|
281
|
+
"--foundation-offline-kit",
|
|
282
|
+
str(prepared.offline_kit),
|
|
283
|
+
"--foundation-release-root",
|
|
284
|
+
str(prepared.release_root),
|
|
285
|
+
"--foundation-bundle-public-key",
|
|
286
|
+
str(prepared.bundle_public_key),
|
|
287
|
+
"--foundation-profile",
|
|
288
|
+
str(prepared.profile),
|
|
289
|
+
"--foundation-variables-file",
|
|
290
|
+
str(foundation_variables),
|
|
291
|
+
*(
|
|
292
|
+
()
|
|
293
|
+
if not create_runner_image
|
|
294
|
+
else (
|
|
295
|
+
"--create-runner-image",
|
|
296
|
+
"--runner-image-terraform",
|
|
297
|
+
str(prepared.terraform),
|
|
298
|
+
)
|
|
299
|
+
),
|
|
300
|
+
"--runner-ssh-private-key",
|
|
301
|
+
str(prepared.ssh_private_key),
|
|
302
|
+
*(("--approval-file", str(approval)) if approval.exists() else ()),
|
|
303
|
+
"--execution-timeout-seconds",
|
|
304
|
+
str(stage_timeout),
|
|
305
|
+
"--output",
|
|
306
|
+
"json",
|
|
307
|
+
)
|
|
308
|
+
try:
|
|
309
|
+
with foundation_output() as stderr:
|
|
310
|
+
foundation_exit = run_foundation_process(
|
|
311
|
+
command,
|
|
312
|
+
cwd=kit.bundle_root,
|
|
313
|
+
env=_standalone_subprocess_environment(
|
|
314
|
+
PYTHONPATH=os.pathsep.join(
|
|
315
|
+
(str(scripts), str(Path(__file__).parent.parent))
|
|
316
|
+
),
|
|
317
|
+
AZURE_SUBSCRIPTION_ID=target.subscription_id,
|
|
318
|
+
AZURE_TENANT_ID=target.tenant_id,
|
|
319
|
+
FDAI_SIGNED_SOURCE_EVIDENCE=source_evidence,
|
|
320
|
+
),
|
|
321
|
+
stdout=subprocess.DEVNULL,
|
|
322
|
+
stderr=stderr,
|
|
323
|
+
timeout=stage_timeout + 15,
|
|
324
|
+
)
|
|
325
|
+
if foundation_exit.returncode not in {0, 2}:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
foundation_failure_summary(
|
|
328
|
+
status_path,
|
|
329
|
+
previous=previous_attempt,
|
|
330
|
+
source_commit=foundation_source,
|
|
331
|
+
run_binding=prepared.run_binding,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
except subprocess.TimeoutExpired as exc:
|
|
335
|
+
raise TimeoutError(
|
|
336
|
+
"standalone Foundation orchestration timed out; inspect retained status before recovery"
|
|
337
|
+
) from exc
|
|
338
|
+
status = current_status(
|
|
339
|
+
status_path,
|
|
340
|
+
previous=previous_attempt,
|
|
341
|
+
source_commit=foundation_source,
|
|
342
|
+
run_binding=prepared.run_binding,
|
|
343
|
+
)
|
|
344
|
+
completed_stages = status.get("completed_stages")
|
|
345
|
+
if (
|
|
346
|
+
status.get("current_stage") == "application-plan"
|
|
347
|
+
and status.get("route") == "private-runner"
|
|
348
|
+
and isinstance(completed_stages, list)
|
|
349
|
+
and "foundation-state" in completed_stages
|
|
350
|
+
):
|
|
351
|
+
foundation = _foundation_result(kit, prepared, status)
|
|
352
|
+
lineage_receipt_digest = _bind_application_to_foundation_lineage(
|
|
353
|
+
kit=kit,
|
|
354
|
+
prepared=prepared,
|
|
355
|
+
status=status,
|
|
356
|
+
lineage_continuation=lineage_continuation,
|
|
357
|
+
tenant_id=target.tenant_id,
|
|
358
|
+
subscription_id=target.subscription_id,
|
|
359
|
+
region=region,
|
|
360
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
361
|
+
)
|
|
362
|
+
deadline.remaining()
|
|
363
|
+
return complete_application(
|
|
364
|
+
kit=kit,
|
|
365
|
+
prepared=prepared,
|
|
366
|
+
status=status,
|
|
367
|
+
scripts=scripts,
|
|
368
|
+
deadline=deadline,
|
|
369
|
+
selected_runtime=selected_runtime,
|
|
370
|
+
trial_token=trial_token,
|
|
371
|
+
application_state_adoption=adoption,
|
|
372
|
+
foundation_state_receipt_digest=str(foundation["foundation_state_receipt_digest"]),
|
|
373
|
+
foundation_adoption_receipt_digest=lineage_receipt_digest,
|
|
374
|
+
catalog_review_profile=(
|
|
375
|
+
catalog_review_profile or CatalogReviewDeploymentProfile.unselected()
|
|
376
|
+
),
|
|
377
|
+
operational_evidence_verifier_input=operational_evidence_verifier_input,
|
|
378
|
+
current_operator_object_id=_current_operator_object_id,
|
|
379
|
+
control_package=control,
|
|
380
|
+
)
|
|
381
|
+
if foundation_exit.returncode != 2:
|
|
382
|
+
raise ValueError("standalone Foundation orchestration failed")
|
|
383
|
+
if lineage_continuation:
|
|
384
|
+
# A new Foundation plan computed from the newer kit must not be approved under the
|
|
385
|
+
# retained revision's lineage; continuation only verifies completed checkpoints.
|
|
386
|
+
raise ValueError(
|
|
387
|
+
"offline kit upgrade requires a completed Foundation and cannot approve a new "
|
|
388
|
+
"Foundation plan under its retained source lineage; inspect retained status"
|
|
389
|
+
)
|
|
390
|
+
approval.unlink(missing_ok=True)
|
|
391
|
+
try:
|
|
392
|
+
with terminal_output("Review the exact Foundation plan", approval=True):
|
|
393
|
+
prompt = subprocess.run(
|
|
394
|
+
(
|
|
395
|
+
sys.executable,
|
|
396
|
+
str(scripts / "genesis_approval_prompt.py"),
|
|
397
|
+
"--status",
|
|
398
|
+
str(status_path),
|
|
399
|
+
"--output",
|
|
400
|
+
str(approval),
|
|
401
|
+
),
|
|
402
|
+
cwd=kit.bundle_root,
|
|
403
|
+
env=_standalone_subprocess_environment(
|
|
404
|
+
PYTHONPATH=os.pathsep.join(
|
|
405
|
+
(str(scripts), str(Path(__file__).parent.parent))
|
|
406
|
+
),
|
|
407
|
+
FDAI_SIGNED_SOURCE_EVIDENCE=source_evidence,
|
|
408
|
+
),
|
|
409
|
+
stdout=sys.stderr,
|
|
410
|
+
check=False,
|
|
411
|
+
timeout=deadline.remaining(600),
|
|
412
|
+
)
|
|
413
|
+
except subprocess.TimeoutExpired as exc:
|
|
414
|
+
raise TimeoutError("standalone Foundation approval prompt timed out") from exc
|
|
415
|
+
if prompt.returncode != 0:
|
|
416
|
+
raise ValueError("standalone Foundation approval was not granted")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _require_feasible_new_aks_target(
|
|
420
|
+
profile: RuntimeDeploymentProfile,
|
|
421
|
+
*,
|
|
422
|
+
region: str,
|
|
423
|
+
deadline: DeploymentDeadline,
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Stop a new AKS installation before its first Azure change when the target is infeasible."""
|
|
426
|
+
|
|
427
|
+
progress_detail("Checking AKS capacity and database availability before the first Azure change")
|
|
428
|
+
preflight = inspect_aks_target(
|
|
429
|
+
profile=profile, region=region, timeout_seconds=deadline.remaining(180)
|
|
430
|
+
)
|
|
431
|
+
if preflight.get("state") != "feasible":
|
|
432
|
+
blockers = preflight.get("blockers")
|
|
433
|
+
names = (
|
|
434
|
+
", ".join(str(item) for item in blockers)
|
|
435
|
+
if isinstance(blockers, list) and blockers
|
|
436
|
+
else "unknown"
|
|
437
|
+
)
|
|
438
|
+
raise ValueError(f"standalone AKS preflight blocked a new installation: {names}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _require_unique_entra_display_names(
|
|
442
|
+
profile: RuntimeDeploymentProfile,
|
|
443
|
+
scripts: Path,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Stop identity-enabled new installations before first Azure effect if names are ambiguous."""
|
|
446
|
+
|
|
447
|
+
if not _identity_add_on_selected(profile):
|
|
448
|
+
return
|
|
449
|
+
sys.path.insert(0, str(scripts))
|
|
450
|
+
try:
|
|
451
|
+
genesis_entra = importlib.import_module("genesis_entra")
|
|
452
|
+
check = getattr(genesis_entra, "check_unique_display_names", None)
|
|
453
|
+
if callable(check):
|
|
454
|
+
check()
|
|
455
|
+
return
|
|
456
|
+
plan_entra = getattr(genesis_entra, "plan_entra", None)
|
|
457
|
+
if callable(plan_entra):
|
|
458
|
+
plan_entra()
|
|
459
|
+
finally:
|
|
460
|
+
sys.path.remove(str(scripts))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _identity_add_on_selected(profile: RuntimeDeploymentProfile) -> bool:
|
|
464
|
+
product = profile.product_profile
|
|
465
|
+
return product.selects(ProductAddOn.READ_ONLY_CONSOLE) or product.selects(
|
|
466
|
+
ProductAddOn.ENTERPRISE_IDENTITY_GOVERNANCE
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def active_azure_target() -> ActiveAzureTarget:
|
|
471
|
+
"""Read the active Azure CLI user target without changing account selection."""
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
completed = subprocess.run(
|
|
475
|
+
(
|
|
476
|
+
"az",
|
|
477
|
+
"account",
|
|
478
|
+
"show",
|
|
479
|
+
*azure_target_selection_arguments(),
|
|
480
|
+
"--query",
|
|
481
|
+
"{subscription_id:id,tenant_id:tenantId,user_type:user.type}",
|
|
482
|
+
"--output",
|
|
483
|
+
"json",
|
|
484
|
+
"--only-show-errors",
|
|
485
|
+
),
|
|
486
|
+
check=False,
|
|
487
|
+
capture_output=True,
|
|
488
|
+
text=True,
|
|
489
|
+
timeout=60,
|
|
490
|
+
)
|
|
491
|
+
except (OSError, subprocess.SubprocessError):
|
|
492
|
+
raise ValueError(
|
|
493
|
+
"Azure authentication read failed; inspect the local login context"
|
|
494
|
+
) from None
|
|
495
|
+
if completed.returncode != 0:
|
|
496
|
+
raise ValueError("Azure authentication is unavailable; run az login first")
|
|
497
|
+
value = json.loads(completed.stdout)
|
|
498
|
+
subscription = value.get("subscription_id") if isinstance(value, dict) else None
|
|
499
|
+
tenant = value.get("tenant_id") if isinstance(value, dict) else None
|
|
500
|
+
if (
|
|
501
|
+
not isinstance(value, dict)
|
|
502
|
+
or value.get("user_type") != "user"
|
|
503
|
+
or not isinstance(subscription, str)
|
|
504
|
+
or _GUID.fullmatch(subscription) is None
|
|
505
|
+
or not isinstance(tenant, str)
|
|
506
|
+
or _GUID.fullmatch(tenant) is None
|
|
507
|
+
):
|
|
508
|
+
raise ValueError("standalone deployment requires an authenticated Azure human")
|
|
509
|
+
require_selected_azure_target(subscription_id=subscription, tenant_id=tenant)
|
|
510
|
+
return ActiveAzureTarget(subscription_id=subscription, tenant_id=tenant)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _standalone_subprocess_environment(
|
|
514
|
+
source: Mapping[str, str] | None = None,
|
|
515
|
+
**overrides: str,
|
|
516
|
+
) -> dict[str, str]:
|
|
517
|
+
"""Return the minimal non-secret environment used by standalone child processes."""
|
|
518
|
+
|
|
519
|
+
inherited = os.environ if source is None else source
|
|
520
|
+
result = {key: inherited[key] for key in _SAFE_ENVIRONMENT_KEYS if key in inherited}
|
|
521
|
+
result.update(overrides)
|
|
522
|
+
return result
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _current_operator_object_id() -> str:
|
|
526
|
+
try:
|
|
527
|
+
completed = subprocess.run(
|
|
528
|
+
(
|
|
529
|
+
"az",
|
|
530
|
+
"ad",
|
|
531
|
+
"signed-in-user",
|
|
532
|
+
"show",
|
|
533
|
+
"--query",
|
|
534
|
+
"id",
|
|
535
|
+
"--output",
|
|
536
|
+
"tsv",
|
|
537
|
+
"--only-show-errors",
|
|
538
|
+
),
|
|
539
|
+
check=False,
|
|
540
|
+
capture_output=True,
|
|
541
|
+
text=True,
|
|
542
|
+
timeout=60,
|
|
543
|
+
)
|
|
544
|
+
except (OSError, subprocess.SubprocessError):
|
|
545
|
+
raise ValueError("authenticated Azure operator read failed; no retry performed") from None
|
|
546
|
+
value = completed.stdout.strip()
|
|
547
|
+
if completed.returncode != 0 or _GUID.fullmatch(value) is None:
|
|
548
|
+
raise ValueError("authenticated Azure operator object ID is unavailable")
|
|
549
|
+
return value
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _bind_application_to_foundation_lineage(
|
|
553
|
+
*,
|
|
554
|
+
kit: DeploymentKit,
|
|
555
|
+
prepared: Any,
|
|
556
|
+
status: dict[str, Any],
|
|
557
|
+
lineage_continuation: bool,
|
|
558
|
+
tenant_id: str,
|
|
559
|
+
subscription_id: str,
|
|
560
|
+
region: str,
|
|
561
|
+
monthly_cost_ceiling: int,
|
|
562
|
+
) -> str | None:
|
|
563
|
+
"""Give the managed host verified evidence that binds a retained Foundation to this kit."""
|
|
564
|
+
|
|
565
|
+
receipt_path = prepared.root / "foundation-adoption-receipt.json"
|
|
566
|
+
if not lineage_continuation:
|
|
567
|
+
if receipt_path.exists() or receipt_path.is_symlink():
|
|
568
|
+
retained = _private_json(receipt_path)
|
|
569
|
+
if retained.get("foundation_source_commit") == kit.source_commit:
|
|
570
|
+
# The kit is back at the Foundation's own revision, so no adoption applies.
|
|
571
|
+
review = prepared.root / "foundation-adoption-review"
|
|
572
|
+
_create_or_validate_private_directory(review)
|
|
573
|
+
receipt_path.rename(review / f"{str(retained.get('receipt_digest'))[:16]}.json")
|
|
574
|
+
return None
|
|
575
|
+
report = status.get("foundation_report")
|
|
576
|
+
plan = report.get("foundation_plan") if isinstance(report, dict) else None
|
|
577
|
+
plan_ref = plan.get("plan_ref") if isinstance(plan, dict) else None
|
|
578
|
+
if (
|
|
579
|
+
not isinstance(plan_ref, str)
|
|
580
|
+
or re.fullmatch(r"foundation-plan-attempt-[1-9][0-9]*", plan_ref) is None
|
|
581
|
+
):
|
|
582
|
+
raise ValueError("retained Foundation plan reference is invalid")
|
|
583
|
+
receipt = run_foundation_transition(
|
|
584
|
+
kit=kit,
|
|
585
|
+
run_root=prepared.root,
|
|
586
|
+
retained_plan_ref=plan_ref,
|
|
587
|
+
transition_plan_ref=None,
|
|
588
|
+
application_source_commit=kit.source_commit,
|
|
589
|
+
kit_manifest_digest=kit.verification.manifest_digest,
|
|
590
|
+
runtime_release_digest=kit.runtime.digest,
|
|
591
|
+
tenant_id=tenant_id,
|
|
592
|
+
subscription_id=subscription_id,
|
|
593
|
+
region=region,
|
|
594
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
595
|
+
transition_runner=default_transition_runner,
|
|
596
|
+
)
|
|
597
|
+
return str(receipt["receipt_digest"])
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _foundation_result(
|
|
601
|
+
kit: DeploymentKit, prepared: Any, status: dict[str, Any]
|
|
602
|
+
) -> dict[str, object]:
|
|
603
|
+
report = status.get("foundation_report")
|
|
604
|
+
handoff = report.get("state_handoff") if isinstance(report, dict) else None
|
|
605
|
+
receipt = handoff.get("receipt_digest") if isinstance(handoff, dict) else None
|
|
606
|
+
if not isinstance(receipt, str) or _DIGEST.fullmatch(receipt) is None:
|
|
607
|
+
raise ValueError("Foundation state handoff receipt is unavailable")
|
|
608
|
+
return {
|
|
609
|
+
"schema_version": "fdai.standalone-azure-deployment.v1",
|
|
610
|
+
"state": "foundation-ready",
|
|
611
|
+
"source_commit": kit.source_commit,
|
|
612
|
+
"kit_manifest_digest": kit.verification.manifest_digest,
|
|
613
|
+
"runtime_release_digest": kit.runtime.digest,
|
|
614
|
+
"foundation_state_receipt_digest": receipt,
|
|
615
|
+
"work_dir": str(prepared.root),
|
|
616
|
+
"next_action": "plan-standalone-application",
|
|
617
|
+
"mutation_performed": True,
|
|
618
|
+
"subscription_ready": False,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _private_json(path: Path) -> dict[str, Any]:
|
|
623
|
+
value = json.loads(read_private_bytes(path, max_bytes=1_048_576))
|
|
624
|
+
if not isinstance(value, dict):
|
|
625
|
+
raise ValueError("standalone deployment status is invalid") # noqa: TRY004 - JSON contract.
|
|
626
|
+
return {str(key): item for key, item in value.items()}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _create_or_validate_private_directory(path: Path) -> None:
|
|
630
|
+
path.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
631
|
+
details = path.lstat()
|
|
632
|
+
if (
|
|
633
|
+
not path.is_absolute()
|
|
634
|
+
or not stat.S_ISDIR(details.st_mode)
|
|
635
|
+
or stat.S_IMODE(details.st_mode) != 0o700
|
|
636
|
+
or details.st_uid != os.geteuid()
|
|
637
|
+
):
|
|
638
|
+
raise PermissionError("standalone deployment directory must be current-UID mode 0700")
|