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,173 @@
1
+ """Read bounded public Linux VM retail prices without Azure identity or mutation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from datetime import UTC, datetime
12
+ from decimal import Decimal, InvalidOperation
13
+ from typing import Any
14
+
15
+ from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
16
+
17
+ _ENDPOINT = "https://prices.azure.com/api/retail/prices"
18
+ _MAX_BYTES = 2 * 1024 * 1024
19
+
20
+
21
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
22
+ def redirect_request(self, *args: Any, **kwargs: Any) -> None:
23
+ raise ValueError("Azure retail price redirects are not supported")
24
+
25
+
26
+ def read_linux_vm_price(*, sku: str, region: str, timeout_seconds: int = 90) -> dict[str, object]:
27
+ """Fetch one complete exact-SKU catalog and select a unique primary USD hourly rate.
28
+
29
+ At most four pages share one deadline. Redirects, foreign continuation URLs,
30
+ repeated pages, malformed responses and ambiguous prices fail without retry.
31
+ """
32
+ if (
33
+ re.fullmatch(r"Standard_[A-Za-z0-9_]{1,80}", sku) is None
34
+ or re.fullmatch(r"[a-z][a-z0-9]{1,40}", region) is None
35
+ ):
36
+ raise ValueError("Azure retail price selection is invalid")
37
+ deadline = DeploymentDeadline(timeout_seconds)
38
+ query = urllib.parse.urlencode(
39
+ {
40
+ "$filter": f"armSkuName eq '{sku}' and armRegionName eq '{region}' and serviceName eq 'Virtual Machines' and priceType eq 'Consumption'",
41
+ "currencyCode": "USD",
42
+ }
43
+ )
44
+ url = f"{_ENDPOINT}?{query}"
45
+ seen: set[str] = set()
46
+ rows: list[object] = []
47
+ evidence = hashlib.sha256()
48
+ opener = urllib.request.build_opener(_NoRedirect())
49
+ for _page in range(4):
50
+ parsed = urllib.parse.urlsplit(url)
51
+ if (
52
+ parsed.scheme != "https"
53
+ or parsed.netloc != "prices.azure.com"
54
+ or parsed.path != "/api/retail/prices"
55
+ or parsed.fragment
56
+ or url in seen
57
+ ):
58
+ raise ValueError("Azure retail price continuation is invalid")
59
+ seen.add(url)
60
+ try:
61
+ with opener.open(url, timeout=deadline.remaining(30)) as response:
62
+ chunks = bytearray()
63
+ while len(chunks) <= _MAX_BYTES:
64
+ deadline.remaining()
65
+ chunk = response.read1(min(65536, _MAX_BYTES + 1 - len(chunks)))
66
+ if not chunk:
67
+ break
68
+ chunks.extend(chunk)
69
+ payload = bytes(chunks)
70
+ except (OSError, urllib.error.URLError):
71
+ raise ValueError(
72
+ "Azure retail price observation unavailable; no retry performed"
73
+ ) from None
74
+ deadline.remaining()
75
+ if len(payload) > _MAX_BYTES:
76
+ raise ValueError("Azure retail price observation exceeds its bound")
77
+ evidence.update(hashlib.sha256(payload).digest())
78
+ page = json.loads(payload, parse_float=Decimal)
79
+ if (
80
+ not isinstance(page, dict)
81
+ or not isinstance(page.get("Items"), list)
82
+ or "NextPageLink" not in page
83
+ or len(page["Items"]) > 1000
84
+ or type(page.get("Count")) is not int
85
+ or page["Count"] != len(page["Items"])
86
+ ):
87
+ raise ValueError("Azure retail price page is invalid")
88
+ rows.extend(page["Items"])
89
+ next_page = page.get("NextPageLink")
90
+ if next_page in (None, ""):
91
+ now = datetime.now(UTC)
92
+ result = select_linux_vm_price(rows, sku=sku, region=region, now=now)
93
+ deadline.remaining()
94
+ return {
95
+ **result,
96
+ "observed_at": now.isoformat(),
97
+ "catalog_digest": evidence.hexdigest(),
98
+ }
99
+ if not isinstance(next_page, str) or len(next_page) > 8192:
100
+ raise ValueError("Azure retail price continuation is invalid")
101
+ url = next_page
102
+ raise ValueError("Azure retail price catalog exceeds the page limit")
103
+
104
+
105
+ def select_linux_vm_price(
106
+ rows: list[object], *, sku: str, region: str, now: datetime
107
+ ) -> dict[str, object]:
108
+ """Select standard Linux consumption, never Windows, Spot, reservation or a foreign meter."""
109
+ if now.tzinfo is None or now.utcoffset() is None:
110
+ raise ValueError("Azure retail price selection requires an aware time")
111
+ matches = []
112
+ for row in rows:
113
+ if not isinstance(row, dict):
114
+ raise ValueError("Azure retail price row is invalid")
115
+ product, meter, sku_name = row.get("productName"), row.get("meterName"), row.get("skuName")
116
+ if (
117
+ not isinstance(product, str)
118
+ or not isinstance(meter, str)
119
+ or not isinstance(sku_name, str)
120
+ ):
121
+ raise ValueError("Azure retail price labels are incomplete")
122
+ if (
123
+ row.get("armSkuName") != sku
124
+ or row.get("armRegionName") != region
125
+ or row.get("serviceName") != "Virtual Machines"
126
+ or row.get("type") != "Consumption"
127
+ or row.get("currencyCode") != "USD"
128
+ or row.get("unitOfMeasure") != "1 Hour"
129
+ or row.get("isPrimaryMeterRegion") is not True
130
+ or row.get("tierMinimumUnits") != 0
131
+ or type(row.get("tierMinimumUnits")) is bool
132
+ or not product.startswith("Virtual Machines ")
133
+ or any(
134
+ token in f"{product} {meter} {sku_name}".casefold()
135
+ for token in ("windows", "spot", "low priority")
136
+ )
137
+ ):
138
+ continue
139
+ try:
140
+ effective = datetime.fromisoformat(
141
+ str(row.get("effectiveStartDate")).replace("Z", "+00:00")
142
+ )
143
+ raw_rate = row.get("retailPrice")
144
+ if isinstance(raw_rate, bool) or not isinstance(raw_rate, (int, float, Decimal)):
145
+ raise ValueError("invalid price")
146
+ rate = Decimal(str(raw_rate))
147
+ if (
148
+ not rate.is_finite()
149
+ or not Decimal(0) < rate <= Decimal(1000000)
150
+ or not -12 <= rate.adjusted() <= 6
151
+ or len(rate.as_tuple().digits) > 18
152
+ ):
153
+ raise ValueError("invalid price")
154
+ except (ValueError, InvalidOperation):
155
+ raise ValueError("Azure retail price or effective time is invalid") from None
156
+ if effective.tzinfo is None:
157
+ raise ValueError("Azure retail price effective time is not timezone aware")
158
+ if effective > now:
159
+ continue
160
+ matches.append((rate, effective))
161
+ if len(matches) != 1:
162
+ raise ValueError("Azure Linux VM retail price is missing or ambiguous")
163
+ rate, effective = matches[0]
164
+ return {
165
+ "sku": sku,
166
+ "region": region,
167
+ "currency": "USD",
168
+ "unit": "hour",
169
+ "hourly_rate": format(rate, "f"),
170
+ "effective_at": effective.isoformat(),
171
+ "pricing_basis": "public-linux-consumption",
172
+ "billing_cap_enforced": False,
173
+ }
@@ -0,0 +1,99 @@
1
+ """Scoped approval-source injection; terminal execution remains the default."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterator
6
+ from contextlib import contextmanager
7
+ from contextvars import ContextVar
8
+ from datetime import UTC, datetime
9
+ from typing import Any
10
+
11
+ ApprovalSource = Callable[[dict[str, object]], dict[str, object]]
12
+ _SOURCE: ContextVar[ApprovalSource | None] = ContextVar("bootstrap_approval_source", default=None)
13
+
14
+
15
+ @contextmanager
16
+ def approval_source(source: ApprovalSource) -> Iterator[None]:
17
+ """Select a human decision adapter for this operation, without changing input()."""
18
+ token = _SOURCE.set(source)
19
+ try:
20
+ yield
21
+ finally:
22
+ _SOURCE.reset(token)
23
+
24
+
25
+ def selected_source() -> ApprovalSource | None:
26
+ return _SOURCE.get()
27
+
28
+
29
+ def require_decision(review: dict[str, object]) -> None:
30
+ """Reject missing, denied, expired or differently bound adapter decisions."""
31
+ source = selected_source()
32
+ if source is None:
33
+ raise ValueError("No injected approval source is selected")
34
+ if datetime.fromisoformat(str(review["expires_at"])) <= datetime.now(UTC):
35
+ raise ValueError("Exact checkpoint review has expired")
36
+ decision = source(dict(review))
37
+ if (
38
+ not isinstance(decision, dict)
39
+ or decision.get("approved") is not True
40
+ or decision.get("digest") != review["digest"]
41
+ or decision.get("confirmation") != review["confirmation_required"]
42
+ or datetime.fromisoformat(str(review["expires_at"])) <= datetime.now(UTC)
43
+ ):
44
+ raise ValueError("Exact checkpoint approval was denied, expired or mismatched")
45
+
46
+
47
+ def approve_application_checkpoint(
48
+ review: dict[str, Any],
49
+ stage: str,
50
+ destructive: int,
51
+ *,
52
+ actor_digest: Callable[[], str],
53
+ ) -> tuple[str, str | None]:
54
+ """Return the exact operation and pre-review actor; None keeps terminal approval."""
55
+ historical_reconciliation = review.get("historical_reconciliation")
56
+ residual_recovery = review.get("residual_recovery")
57
+ residual_successor = review.get("residual_successor")
58
+ operation = (
59
+ str(residual_successor["operation"])
60
+ if isinstance(residual_successor, dict)
61
+ else str(residual_recovery["operation"])
62
+ if isinstance(residual_recovery, dict)
63
+ else str(historical_reconciliation["operation"])
64
+ if isinstance(historical_reconciliation, dict)
65
+ else stage
66
+ )
67
+ if selected_source() is None:
68
+ return operation, None
69
+ actor_before = actor_digest()
70
+ expected = f"{operation}-apply"
71
+ require_decision(
72
+ {
73
+ "digest": str(review["review_digest"]),
74
+ "title": f"Approve exact {operation} plan",
75
+ "details": [
76
+ f"Plan digest: {review['plan_digest']}",
77
+ f"Source commit: {review['source_commit']}",
78
+ f"Delete or replacement actions: {destructive}",
79
+ "Action counts: "
80
+ + ", ".join(
81
+ f"{action}={count}"
82
+ for action, count in sorted(review["summary"]["action_counts"].items())
83
+ ),
84
+ *(
85
+ f"Resource type {name}: {count}"
86
+ for name, count in sorted(
87
+ review["summary"].get("resource_type_counts", {}).items()
88
+ )
89
+ ),
90
+ *(
91
+ f"{','.join(change['actions'])}: {change['address']}"
92
+ for change in review["summary"].get("resource_changes", [])
93
+ ),
94
+ ],
95
+ "confirmation_required": f"{expected}-destructive" if destructive else expected,
96
+ "expires_at": str(review["expires_at"]),
97
+ }
98
+ )
99
+ return operation, actor_before
@@ -0,0 +1,321 @@
1
+ """Private, bounded, atomic IPC for the customer-side first-install worker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fcntl
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ import time
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+ from datetime import UTC, datetime
14
+ from pathlib import Path
15
+ from typing import Any
16
+ from uuid import UUID, uuid4
17
+
18
+ from fdai_deployment_cli.contracts import load_json_object
19
+
20
+ MAX_BYTES = 65536
21
+ STALE_SECONDS = 12
22
+ INSTALL_FIELDS = {
23
+ "subscription_id",
24
+ "region",
25
+ "monthly_cost_ceiling",
26
+ "artifact_source",
27
+ "source_root",
28
+ "hub_public_key",
29
+ }
30
+
31
+
32
+ def prepare_directory(root: Path) -> None:
33
+ """Create an owner-only IPC root; refuse symlinks and unsafe existing paths."""
34
+ root = root.absolute()
35
+ for path in (*reversed(root.parents), root):
36
+ if path.is_symlink():
37
+ raise ValueError("Bootstrap directory must not contain symlinks")
38
+ root.mkdir(mode=0o700, parents=True, exist_ok=True)
39
+ info = root.lstat()
40
+ if (
41
+ not stat.S_ISDIR(info.st_mode)
42
+ or info.st_uid != os.getuid()
43
+ or stat.S_IMODE(info.st_mode) != 0o700
44
+ ):
45
+ raise ValueError("Bootstrap directory must be owned by this user with mode 0700")
46
+
47
+
48
+ def _read(root: Path, name: str = "channel.json") -> dict[str, Any]:
49
+ path = root / name
50
+ try:
51
+ fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
52
+ except FileNotFoundError:
53
+ return {}
54
+ except OSError as exc:
55
+ raise ValueError("Bootstrap private file is unavailable or unsafe") from exc
56
+ with os.fdopen(fd, "rb") as stream:
57
+ info = os.fstat(stream.fileno())
58
+ if (
59
+ not stat.S_ISREG(info.st_mode)
60
+ or info.st_uid != os.getuid()
61
+ or stat.S_IMODE(info.st_mode) != 0o600
62
+ or info.st_nlink != 1
63
+ ):
64
+ raise ValueError("Bootstrap private files require mode 0600 and one owner")
65
+ raw = stream.read(MAX_BYTES + 1)
66
+ try:
67
+ value = load_json_object(raw, label="Bootstrap state", max_bytes=MAX_BYTES, strict=True)
68
+ except (ValueError, UnicodeError) as exc:
69
+ raise ValueError("Bootstrap state is malformed; preserve the private directory") from exc
70
+ if len(raw) > MAX_BYTES or not isinstance(value, dict):
71
+ raise ValueError("Bootstrap state exceeds its bounded object schema")
72
+ return value
73
+
74
+
75
+ def _write(root: Path, value: dict[str, object], name: str = "channel.json") -> None:
76
+ raw = json.dumps(value, separators=(",", ":"), allow_nan=False).encode()
77
+ if len(raw) > MAX_BYTES:
78
+ raise ValueError("Bootstrap state exceeds its size bound")
79
+ target = root / name
80
+ if target.is_symlink():
81
+ raise ValueError("Bootstrap private files must not be symlinks")
82
+ staging = root / f".write-{uuid4().hex}"
83
+ fd = os.open(staging, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
84
+ try:
85
+ with os.fdopen(fd, "wb") as stream:
86
+ stream.write(raw)
87
+ stream.flush()
88
+ os.fsync(stream.fileno())
89
+ os.replace(staging, target)
90
+ directory = os.open(root, os.O_RDONLY | os.O_DIRECTORY)
91
+ try:
92
+ os.fsync(directory)
93
+ finally:
94
+ os.close(directory)
95
+ finally:
96
+ staging.unlink(missing_ok=True)
97
+
98
+
99
+ @contextmanager
100
+ def locked(root: Path) -> Iterator[None]:
101
+ prepare_directory(root)
102
+ fd = os.open(root / "channel.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
103
+ try:
104
+ info = os.fstat(fd)
105
+ if (
106
+ not stat.S_ISREG(info.st_mode)
107
+ or stat.S_IMODE(info.st_mode) != 0o600
108
+ or info.st_uid != os.getuid()
109
+ or info.st_nlink != 1
110
+ ):
111
+ raise ValueError("Bootstrap lock is unsafe")
112
+ deadline = time.monotonic() + 5
113
+ while True:
114
+ try:
115
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
116
+ break
117
+ except BlockingIOError as exc:
118
+ if time.monotonic() >= deadline:
119
+ raise ValueError(
120
+ "Bootstrap state is busy; wait for the current private update."
121
+ ) from exc
122
+ time.sleep(0.01)
123
+ yield
124
+ finally:
125
+ os.close(fd)
126
+
127
+
128
+ def initial_status() -> dict[str, object]:
129
+ return {
130
+ "phase": "idle",
131
+ "request_id": None,
132
+ "action": None,
133
+ "message": "Sign in to Azure to begin first installation.",
134
+ "error_code": None,
135
+ "accounts": [],
136
+ "review": None,
137
+ "deployment_ready": False,
138
+ "console_url": None,
139
+ }
140
+
141
+
142
+ def _status(record: dict[str, object]) -> dict[str, object]:
143
+ stored = record.get("status")
144
+ result = initial_status()
145
+ if isinstance(stored, dict):
146
+ result.update({key: stored[key] for key in result if key in stored})
147
+ if record.get("active_request_id") is not None and result["phase"] in {
148
+ "idle",
149
+ "ready",
150
+ "blocked",
151
+ "failed",
152
+ }:
153
+ result.update(
154
+ phase="working",
155
+ deployment_ready=False,
156
+ console_url=None,
157
+ message="Finalizing local executor cleanup before accepting another request.",
158
+ )
159
+ heartbeat = record.get("heartbeat")
160
+ if result["phase"] in {"working", "review"} and (
161
+ not isinstance(heartbeat, (int, float)) or time.time() - heartbeat > STALE_SECONDS
162
+ ):
163
+ result.update(
164
+ phase="interrupted",
165
+ review=None,
166
+ deployment_ready=False,
167
+ console_url=None,
168
+ error_code="worker_interrupted",
169
+ message="Worker interrupted. Inspect retained state before explicitly resuming.",
170
+ )
171
+ if result["phase"] == "ready" and result["deployment_ready"] is not True:
172
+ result.update(phase="blocked", message="Independent deployment acceptance is unavailable.")
173
+ return result
174
+
175
+
176
+ def read_status(root: Path) -> dict[str, object]:
177
+ """Read only allowlisted setup metadata, never engine output or Azure credentials."""
178
+ with locked(root):
179
+ return _status(_read(root))
180
+
181
+
182
+ def validate_request(action: str, payload: dict[str, object]) -> dict[str, object]:
183
+ if (
184
+ not isinstance(action, str)
185
+ or action not in {"login", "accounts", "install", "resume", "inspect"}
186
+ or not isinstance(payload, dict)
187
+ ):
188
+ raise ValueError("Unsupported Bootstrap action or payload")
189
+ try:
190
+ encoded = json.dumps(payload, allow_nan=False).encode()
191
+ except (ValueError, TypeError, RecursionError) as exc:
192
+ raise ValueError("Bootstrap payload must be a bounded JSON object") from exc
193
+ if len(encoded) > 16384:
194
+ raise ValueError("Bootstrap payload exceeds its size bound")
195
+ if action != "install":
196
+ if payload:
197
+ raise ValueError("This action accepts no replacement inputs")
198
+ return {}
199
+ if set(payload) - INSTALL_FIELDS:
200
+ raise ValueError("Unknown installation fields; use only the documented setup inputs")
201
+ subscription = payload.get("subscription_id")
202
+ try:
203
+ if not isinstance(subscription, str) or str(UUID(subscription)) != subscription.lower():
204
+ raise ValueError
205
+ except ValueError as exc:
206
+ raise ValueError("Select one valid Azure subscription UUID") from exc
207
+ region, ceiling = payload.get("region"), payload.get("monthly_cost_ceiling")
208
+ if not isinstance(region, str) or re.fullmatch(r"[a-z][a-z0-9]{1,31}", region) is None:
209
+ raise ValueError("Choose a valid Azure region token")
210
+ if type(ceiling) is not int or ceiling <= 0 or ceiling > 1_000_000_000:
211
+ raise ValueError("Monthly cost ceiling must be a positive bounded integer")
212
+ result = {**payload, "artifact_source": payload.get("artifact_source", "hub")}
213
+ if not isinstance(result["artifact_source"], str) or result["artifact_source"] not in {
214
+ "hub",
215
+ "source",
216
+ }:
217
+ raise ValueError("Artifact source must be hub or source")
218
+ source = payload.get("source_root")
219
+ if source is not None and (
220
+ not isinstance(source, str) or not source or len(source) > 4096 or "\0" in source
221
+ ):
222
+ raise ValueError("Source root must be a local checkout path")
223
+ key = payload.get("hub_public_key")
224
+ if result["artifact_source"] == "source":
225
+ if not source or key is not None:
226
+ raise ValueError("Source mode requires source_root and does not accept a Hub key")
227
+ else:
228
+ if not isinstance(key, str) or len(key) > 4096:
229
+ raise ValueError("Hub mode requires an independently supplied Ed25519 public PEM")
230
+ from fdai_deployment_cli.hub_image_trust import public_key
231
+
232
+ try:
233
+ public_key(key.encode())
234
+ except (ValueError, TypeError) as exc:
235
+ raise ValueError(
236
+ "Hub trust requires an Ed25519 public PEM, never a private key"
237
+ ) from exc
238
+ return result
239
+
240
+
241
+ def submit_request(root: Path, action: str, payload: dict[str, object]) -> dict[str, object]:
242
+ payload = validate_request(action, payload)
243
+ with locked(root):
244
+ record = _read(root)
245
+ status = _status(record)
246
+ stored_status = record.get("status", {})
247
+ if (
248
+ record.get("pending") is not None
249
+ or record.get("active_request_id") is not None
250
+ or stored_status.get("phase") in {"working", "review"}
251
+ ):
252
+ raise ValueError(
253
+ "A Bootstrap request is already pending or running. "
254
+ "If its worker is interrupted, restart setup before explicit resume."
255
+ )
256
+ if status["phase"] == "interrupted" and action not in {
257
+ "inspect",
258
+ "resume",
259
+ "login",
260
+ "accounts",
261
+ }:
262
+ raise ValueError(
263
+ "Inspect or resume the interrupted installation before starting other work"
264
+ )
265
+ request_id = str(uuid4())
266
+ status.update(
267
+ phase="working",
268
+ request_id=request_id,
269
+ action=action,
270
+ review=None,
271
+ error_code=None,
272
+ message="Request queued for the local customer executor.",
273
+ deployment_ready=False,
274
+ console_url=None,
275
+ )
276
+ record.update(
277
+ status=status,
278
+ heartbeat=time.time(),
279
+ decision=None,
280
+ pending={"id": request_id, "action": action, "payload": payload},
281
+ )
282
+ _write(root, record)
283
+ return dict(status)
284
+
285
+
286
+ def submit_decision(root: Path, payload: dict[str, object]) -> dict[str, object]:
287
+ fields = {"request_id", "review_id", "digest", "approved", "confirmation"}
288
+ if (
289
+ not isinstance(payload, dict)
290
+ or set(payload) != fields
291
+ or type(payload.get("approved")) is not bool
292
+ or any(not isinstance(payload.get(key), str) for key in fields - {"approved"})
293
+ ):
294
+ raise ValueError(
295
+ "Decision requires exact request, review, digest, approval and confirmation"
296
+ )
297
+ with locked(root):
298
+ record = _read(root)
299
+ status = _status(record)
300
+ review = status.get("review")
301
+ if (
302
+ status["phase"] != "review"
303
+ or not isinstance(review, dict)
304
+ or record.get("decision") is not None
305
+ or payload["request_id"] != status["request_id"]
306
+ or payload["review_id"] != review["id"]
307
+ or payload["digest"] != review["digest"]
308
+ ):
309
+ raise ValueError("Decision does not match the current pending review")
310
+ expires = datetime.fromisoformat(str(review["expires_at"]))
311
+ if expires <= datetime.now(UTC):
312
+ raise ValueError("Review expired; explicitly resume to obtain a fresh exact plan")
313
+ if payload["approved"] and payload["confirmation"] != review["confirmation_required"]:
314
+ raise ValueError("Type the exact confirmation shown for this checkpoint")
315
+ record["decision"] = payload
316
+ status.update(
317
+ phase="working", review=None, message="Decision submitted to the exact checkpoint."
318
+ )
319
+ record["status"] = status
320
+ _write(root, record)
321
+ return dict(status)