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.
Files changed (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,442 @@
1
+ """Prepare the managed-host application workspace from verified private inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from fdai_deployment_cli.application_state_adoption import ApplicationStateAdoption
11
+ from fdai_deployment_cli.catalog_review_profile import (
12
+ CatalogReviewDeploymentProfile,
13
+ stage_catalog_review_profile,
14
+ )
15
+ from fdai_deployment_cli.control_package import PACKAGE_ROOT, ControlPackage
16
+ from fdai_deployment_cli.development_authority_profile import (
17
+ DevelopmentAuthorityDeploymentProfile,
18
+ stage_development_authority_profile,
19
+ )
20
+ from fdai_deployment_cli.foundation_adoption_transport import stage_foundation_context
21
+ from fdai_deployment_cli.hub_image_trust import HubImageSelection, load_selection
22
+ from fdai_deployment_cli.notification_recipients_input import (
23
+ NotificationRecipientsDeploymentInput,
24
+ stage_notification_recipients,
25
+ )
26
+ from fdai_deployment_cli.operational_evidence_verifier_input import (
27
+ OperationalEvidenceVerifierDeploymentInput,
28
+ stage_operational_evidence_verifier_input,
29
+ )
30
+ from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
31
+
32
+
33
+ def prepare_remote(
34
+ tunnel: Any,
35
+ *,
36
+ remote_root: str,
37
+ remote_archive: str,
38
+ archive: Path | None,
39
+ archive_digest: str | None,
40
+ handoff_path: Path,
41
+ remote_handoff: str,
42
+ entra_path: Path | None,
43
+ remote_entra: str | None,
44
+ app_work: str,
45
+ source_archive: Path | None = None,
46
+ source_archive_digest: str | None = None,
47
+ source_receiver: Path | None = None,
48
+ source_receiver_digest: str | None = None,
49
+ source_snapshot_digest: str | None = None,
50
+ source_runtime_support: Path | None = None,
51
+ source_runtime_support_digest: str | None = None,
52
+ runtime_profile: RuntimeDeploymentProfile | None = None,
53
+ application_state_adoption: ApplicationStateAdoption | None = None,
54
+ remote_adoption_state: str = "",
55
+ remote_adoption_models: str = "",
56
+ remote_adoption_descriptor: str = "",
57
+ timeout_seconds: int,
58
+ catalog_review_profile: CatalogReviewDeploymentProfile | None = None,
59
+ operational_evidence_verifier_input: OperationalEvidenceVerifierDeploymentInput | None = None,
60
+ notification_recipients: NotificationRecipientsDeploymentInput | None = None,
61
+ development_authority_profile: DevelopmentAuthorityDeploymentProfile | None = None,
62
+ clear_development_authority_profile: bool = False,
63
+ control_package: ControlPackage | None = None,
64
+ hub_selection: HubImageSelection | None = None,
65
+ ) -> dict[str, object]:
66
+ """Transfer exact inputs and invoke the value-free host preparation command.
67
+
68
+ With a verified ``control_package``, the host installs ``fdai-deployment-cli`` only from
69
+ that signed wheelhouse after a remote digest match; the kit still supplies every runtime
70
+ payload. Without it, the host installs the CLI from the kit wheels.
71
+ """
72
+
73
+ selected_runtime = runtime_profile or RuntimeDeploymentProfile.create(
74
+ runtime_platform="container-apps",
75
+ database_placement="postgres-flex",
76
+ )
77
+ source_mode = source_archive is not None
78
+ if hub_selection is not None:
79
+ hub_selection = load_selection(hub_selection.path, hub_selection.digest)
80
+ if not source_mode:
81
+ raise ValueError("Hub images cannot be transferred in offline-kit mode")
82
+ if source_mode:
83
+ if not all(
84
+ value is not None
85
+ for value in (
86
+ source_archive_digest,
87
+ source_receiver,
88
+ source_receiver_digest,
89
+ source_snapshot_digest,
90
+ source_runtime_support,
91
+ source_runtime_support_digest,
92
+ )
93
+ ):
94
+ raise ValueError("source managed-host preparation inputs are incomplete")
95
+ if archive is not None or archive_digest is not None or control_package is not None:
96
+ raise ValueError("source managed-host preparation cannot use kit inputs")
97
+ elif source_runtime_support is not None or source_runtime_support_digest is not None:
98
+ raise ValueError("kit managed-host preparation cannot use source runtime support")
99
+ elif archive is None or archive_digest is None:
100
+ raise ValueError("kit managed-host preparation inputs are incomplete")
101
+ created = tunnel.ssh(("install", "-d", "-m", "0700", remote_root), timeout=60)
102
+ if created.returncode != 0:
103
+ raise ValueError("standalone remote work directory is unavailable")
104
+ removed = tunnel.ssh(("rm", "-f", "--", remote_archive), timeout=60)
105
+ if removed.returncode != 0:
106
+ raise ValueError("standalone remote archive reset failed")
107
+ remote_source_archive = (
108
+ f"{remote_root}/source-transfer.tar.gz"
109
+ if source_mode and source_archive is not None and source_archive.name.endswith(".tar.gz")
110
+ else f"{remote_root}/source-transfer.tar"
111
+ )
112
+ remote_source_receiver = f"{remote_root}/source-receiver.pyz"
113
+ remote_runtime_support = f"{remote_root}/source-runtime-support.tar"
114
+ if source_mode:
115
+ assert source_archive is not None
116
+ assert source_receiver is not None
117
+ assert source_runtime_support is not None
118
+ tunnel.copy_to(source_archive, remote_source_archive, timeout=min(1800, timeout_seconds))
119
+ tunnel.copy_to(source_receiver, remote_source_receiver, timeout=300)
120
+ tunnel.copy_to(source_runtime_support, remote_runtime_support, timeout=600)
121
+ else:
122
+ assert archive is not None
123
+ tunnel.copy_to(archive, remote_archive, timeout=min(1800, timeout_seconds))
124
+ foundation = stage_foundation_context(
125
+ tunnel,
126
+ handoff_path,
127
+ remote_handoff,
128
+ entra_path,
129
+ remote_entra,
130
+ remote_root,
131
+ )
132
+ if application_state_adoption is not None:
133
+ if not all((remote_adoption_state, remote_adoption_models, remote_adoption_descriptor)):
134
+ raise ValueError("standalone application adoption destinations are incomplete")
135
+ tunnel.copy_to(application_state_adoption.state, remote_adoption_state, timeout=300)
136
+ tunnel.copy_to(
137
+ application_state_adoption.resolved_models, remote_adoption_models, timeout=120
138
+ )
139
+ tunnel.copy_to(
140
+ application_state_adoption.descriptor, remote_adoption_descriptor, timeout=120
141
+ )
142
+ if source_mode:
143
+ assert source_archive_digest is not None
144
+ assert source_receiver_digest is not None
145
+ archive_digest_result = tunnel.ssh(("sha256sum", remote_source_archive), timeout=300)
146
+ receiver_digest_result = tunnel.ssh(("sha256sum", remote_source_receiver), timeout=120)
147
+ requirements_digest_result = tunnel.ssh(("sha256sum", remote_runtime_support), timeout=120)
148
+ if (
149
+ archive_digest_result.returncode != 0
150
+ or archive_digest_result.stdout.split(maxsplit=1)[0] != source_archive_digest
151
+ or receiver_digest_result.returncode != 0
152
+ or receiver_digest_result.stdout.split(maxsplit=1)[0] != source_receiver_digest
153
+ or requirements_digest_result.returncode != 0
154
+ or requirements_digest_result.stdout.split(maxsplit=1)[0]
155
+ != source_runtime_support_digest
156
+ ):
157
+ raise ValueError("source transport digest differs")
158
+ install_cli = _source_cli_installation(remote_root)
159
+ else:
160
+ digest = tunnel.ssh(("sha256sum", remote_archive), timeout=300)
161
+ if digest.returncode != 0 or digest.stdout.split(maxsplit=1)[0] != archive_digest:
162
+ raise ValueError("standalone transport archive digest differs")
163
+ install_cli = _kit_cli_installation(remote_root)
164
+ hub_arguments: tuple[str, ...] = ()
165
+ if hub_selection is not None:
166
+ remote_selection = f"{remote_root}/hub-selection.json"
167
+ tunnel.copy_to(hub_selection.path, remote_selection, timeout=120)
168
+ observed = tunnel.ssh(("sha256sum", remote_selection), timeout=120)
169
+ if (
170
+ observed.returncode != 0
171
+ or not observed.stdout.split()
172
+ or observed.stdout.split()[0] != hub_selection.digest
173
+ ):
174
+ raise ValueError("Hub selection transfer digest differs")
175
+ hub_arguments = (
176
+ "--hub-selection",
177
+ remote_selection,
178
+ "--hub-selection-digest",
179
+ hub_selection.digest,
180
+ )
181
+ if control_package is not None:
182
+ install_cli = _stage_control_package(tunnel, remote_root, control_package)
183
+ try:
184
+ catalog_review_arguments = stage_catalog_review_profile(
185
+ catalog_review_profile or CatalogReviewDeploymentProfile.unselected(),
186
+ tunnel=tunnel,
187
+ prepared_root=handoff_path.parent,
188
+ remote_root=remote_root,
189
+ )
190
+ verifier_arguments = stage_operational_evidence_verifier_input(
191
+ operational_evidence_verifier_input,
192
+ tunnel=tunnel,
193
+ prepared_root=handoff_path.parent,
194
+ remote_root=remote_root,
195
+ )
196
+ notification_recipients_arguments = stage_notification_recipients(
197
+ notification_recipients,
198
+ tunnel=tunnel,
199
+ prepared_root=handoff_path.parent,
200
+ remote_root=remote_root,
201
+ )
202
+ development_authority_arguments = stage_development_authority_profile(
203
+ development_authority_profile,
204
+ tunnel=tunnel,
205
+ prepared_root=handoff_path.parent,
206
+ remote_root=remote_root,
207
+ )
208
+ prepare_arguments = (
209
+ f"{remote_root}/venv/bin/python",
210
+ "-m",
211
+ "fdai_deployment_cli.standalone_host",
212
+ "--work-dir",
213
+ app_work,
214
+ *(
215
+ (
216
+ "prepare-source",
217
+ "--source-snapshot",
218
+ f"{remote_root}/source-snapshot",
219
+ "--source-snapshot-digest",
220
+ str(source_snapshot_digest),
221
+ "--runtime-support",
222
+ remote_runtime_support,
223
+ "--runtime-support-digest",
224
+ str(source_runtime_support_digest),
225
+ )
226
+ if source_mode
227
+ else ("prepare", "--kit", f"{remote_root}/kit")
228
+ ),
229
+ "--handoff",
230
+ foundation.handoff,
231
+ *(("--entra", foundation.entra) if foundation.entra is not None else ()),
232
+ *foundation.adoption_arguments,
233
+ "--runtime-platform",
234
+ selected_runtime.runtime_platform.value,
235
+ "--database-placement",
236
+ selected_runtime.database_placement.value,
237
+ "--system-node-count",
238
+ str(selected_runtime.system_node_count),
239
+ "--system-node-sku",
240
+ selected_runtime.system_node_sku,
241
+ "--user-node-min-count",
242
+ str(selected_runtime.user_node_min_count),
243
+ "--user-node-max-count",
244
+ str(selected_runtime.user_node_max_count),
245
+ "--user-node-sku",
246
+ selected_runtime.user_node_sku,
247
+ *(
248
+ ("--database-sku", selected_runtime.database_sku)
249
+ if selected_runtime.database_sku is not None
250
+ else ()
251
+ ),
252
+ *(
253
+ value
254
+ for add_on in selected_runtime.product_profile.add_ons
255
+ for value in ("--product-add-on", add_on.value)
256
+ ),
257
+ *(
258
+ value
259
+ for source in selected_runtime.product_profile.observation_permissions.selected_sources
260
+ for value in ("--observation-source", source.value)
261
+ ),
262
+ *catalog_review_arguments,
263
+ *hub_arguments,
264
+ *verifier_arguments,
265
+ *notification_recipients_arguments,
266
+ *development_authority_arguments,
267
+ *(("--clear-development-profile",) if clear_development_authority_profile else ()),
268
+ *(
269
+ (
270
+ "--adoption-state",
271
+ remote_adoption_state,
272
+ "--adoption-models",
273
+ remote_adoption_models,
274
+ "--adoption-descriptor",
275
+ remote_adoption_descriptor,
276
+ )
277
+ if application_state_adoption is not None
278
+ else ()
279
+ ),
280
+ )
281
+ commands = (
282
+ *(
283
+ (
284
+ ("clean-source", ("rm", "-rf", "--", f"{remote_root}/source-snapshot"), 300),
285
+ (
286
+ "receive-source",
287
+ (
288
+ "python3",
289
+ remote_source_receiver,
290
+ "--archive",
291
+ remote_source_archive,
292
+ "--destination",
293
+ f"{remote_root}/source-snapshot",
294
+ "--archive-digest",
295
+ str(source_archive_digest),
296
+ "--snapshot-digest",
297
+ str(source_snapshot_digest),
298
+ ),
299
+ 1800,
300
+ ),
301
+ )
302
+ if source_mode
303
+ else (
304
+ ("clean-kit", ("rm", "-rf", "--", f"{remote_root}/kit"), 300),
305
+ ("extract-kit", ("tar", "-xzf", remote_archive, "-C", remote_root), 1800),
306
+ )
307
+ ),
308
+ ("clean-venv", ("rm", "-rf", "--", f"{remote_root}/venv"), 300),
309
+ ("create-venv", ("python3", "-m", "venv", f"{remote_root}/venv"), 300),
310
+ *install_cli,
311
+ ("create-workdir", ("install", "-d", "-m", "0700", app_work), 60),
312
+ ("prepare", prepare_arguments, 1800),
313
+ )
314
+ setup = None
315
+ for step, command, limit in commands:
316
+ setup = tunnel.ssh(command, timeout=min(limit, timeout_seconds))
317
+ if setup.returncode != 0:
318
+ raise ValueError(f"standalone managed-host preparation failed: step={step}")
319
+ except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as setup_failure:
320
+ try:
321
+ _cleanup_catalog_review_staging(tunnel, remote_root=remote_root)
322
+ except ValueError as cleanup_failure:
323
+ raise cleanup_failure from setup_failure
324
+ raise
325
+ _cleanup_catalog_review_staging(tunnel, remote_root=remote_root)
326
+ if setup is None:
327
+ raise ValueError("standalone managed-host preparation returned no result")
328
+ try:
329
+ result = json.loads(setup.stdout)
330
+ except json.JSONDecodeError as exc:
331
+ raise ValueError("standalone managed-host preparation returned invalid output") from exc
332
+ if (
333
+ not isinstance(result, dict)
334
+ or result.get("state") != "prepared"
335
+ or type(result.get("focused_private_access")) is not bool
336
+ ):
337
+ raise ValueError("standalone managed-host preparation result is invalid")
338
+ return {str(key): value for key, value in result.items()}
339
+
340
+
341
+ def _kit_cli_installation(remote_root: str) -> tuple[tuple[str, tuple[str, ...], int], ...]:
342
+ return (
343
+ (
344
+ "install-kit-cli",
345
+ (
346
+ f"{remote_root}/venv/bin/pip",
347
+ "install",
348
+ "--no-index",
349
+ "--no-cache-dir",
350
+ "--find-links",
351
+ f"{remote_root}/kit/python",
352
+ "fdai-deployment-cli",
353
+ ),
354
+ 900,
355
+ ),
356
+ )
357
+
358
+
359
+ def _source_cli_installation(remote_root: str) -> tuple[tuple[str, tuple[str, ...], int], ...]:
360
+ return (
361
+ (
362
+ "install-source-cli",
363
+ (
364
+ f"{remote_root}/venv/bin/pip",
365
+ "install",
366
+ "--no-cache-dir",
367
+ # The CLI pins the workspace contracts package, which no index publishes.
368
+ f"{remote_root}/source-snapshot/tree/packages/service-contracts",
369
+ f"{remote_root}/source-snapshot/tree/packages/deployment-cli",
370
+ ),
371
+ 900,
372
+ ),
373
+ )
374
+
375
+
376
+ def _stage_control_package(
377
+ tunnel: Any, remote_root: str, control_package: ControlPackage
378
+ ) -> tuple[tuple[str, tuple[str, ...], int], ...]:
379
+ """Copy the locally verified wheelhouse and require the same bytes on the host."""
380
+
381
+ remote_control = f"{remote_root}/control.tar.gz"
382
+ control_root = f"{remote_root}/control"
383
+ if tunnel.ssh(("rm", "-f", "--", remote_control), timeout=60).returncode != 0:
384
+ raise ValueError("standalone control package reset failed")
385
+ tunnel.copy_to(control_package.archive, remote_control, timeout=300)
386
+ digest = tunnel.ssh(("sha256sum", remote_control), timeout=120)
387
+ if (
388
+ digest.returncode != 0
389
+ or digest.stdout.split(maxsplit=1)[0] != control_package.archive_digest
390
+ ):
391
+ raise ValueError("standalone control package digest differs")
392
+ package = f"{control_root}/{PACKAGE_ROOT}"
393
+ return (
394
+ ("clean-control-package", ("rm", "-rf", "--", control_root), 120),
395
+ ("create-control-package-dir", ("install", "-d", "-m", "0700", control_root), 60),
396
+ ("extract-control-package", ("tar", "-xzf", remote_control, "-C", control_root), 300),
397
+ (
398
+ "install-control-package",
399
+ (
400
+ f"{remote_root}/venv/bin/pip",
401
+ "install",
402
+ "--no-index",
403
+ "--no-cache-dir",
404
+ "--find-links",
405
+ f"{package}/wheels",
406
+ "--requirement",
407
+ f"{package}/requirements.txt",
408
+ ),
409
+ 900,
410
+ ),
411
+ )
412
+
413
+
414
+ def _cleanup_catalog_review_staging(tunnel: Any, *, remote_root: str) -> None:
415
+ """Require removal and absence readback for both staged credential files."""
416
+
417
+ try:
418
+ cleanup = tunnel.ssh(
419
+ (
420
+ "rm",
421
+ "-f",
422
+ "--",
423
+ f"{remote_root}/catalog-review-profile.json",
424
+ f"{remote_root}/catalog-review-private-key.pem",
425
+ ),
426
+ timeout=60,
427
+ )
428
+ profile_absent = tunnel.ssh(
429
+ ("test", "!", "-e", f"{remote_root}/catalog-review-profile.json"),
430
+ timeout=60,
431
+ )
432
+ key_absent = tunnel.ssh(
433
+ ("test", "!", "-e", f"{remote_root}/catalog-review-private-key.pem"),
434
+ timeout=60,
435
+ )
436
+ except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as exc:
437
+ raise ValueError("standalone catalog review profile cleanup is incomplete") from exc
438
+ if any(result.returncode != 0 for result in (cleanup, profile_absent, key_absent)):
439
+ raise ValueError("standalone catalog review profile cleanup is incomplete")
440
+
441
+
442
+ __all__ = ["prepare_remote"]